From e50431d27c0395849b12159481d5e5f0f04be242 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 10 Jul 2019 16:51:59 -0500 Subject: [PATCH 0001/1212] WIP - Move to the new hyperdrive daemon --- app/background-process.js | 6 +----- app/background-process/ui/permissions.js | 2 +- app/background-process/ui/view-manager.js | 7 ++++--- app/dat-daemon.js | 20 -------------------- 4 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 app/dat-daemon.js diff --git a/app/background-process.js b/app/background-process.js index a7cba9d900..82c1920742 100644 --- a/app/background-process.js +++ b/app/background-process.js @@ -85,9 +85,6 @@ app.on('open-file', (e, filepath) => { }) app.on('ready', async function () { - // start the daemon process - var datDaemonProcess = await childProcesses.spawn('dat-daemon', './dat-daemon.js') - portForwarder.setup() // setup core @@ -109,8 +106,7 @@ app.on('ready', async function () { }, rpcAPI: rpc, downloadsWebAPI: downloads.WEBAPI, - browserWebAPI: beakerBrowser.WEBAPI, - datDaemonProcess + browserWebAPI: beakerBrowser.WEBAPI }) // base diff --git a/app/background-process/ui/permissions.js b/app/background-process/ui/permissions.js index a44b63cf06..ebe19d167b 100644 --- a/app/background-process/ui/permissions.js +++ b/app/background-process/ui/permissions.js @@ -76,7 +76,7 @@ export async function checkLabsPerm ({perm, labApi, apiDocsUrl, sender}) { let isOptedIn = false let archive = dat.library.getArchive(key) if (archive) { - let {checkoutFS} = dat.library.getArchiveCheckout(archive, urlp.version) + let {checkoutFS} = await dat.library.getArchiveCheckout(archive, urlp.version) let manifest = await pda.readManifest(checkoutFS).catch(_ => {}) let apis = _get(manifest, 'experimental.apis') if (apis && Array.isArray(apis)) { diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index 05c88dd512..bc44fcb60f 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -489,11 +489,12 @@ class View { // live reloading // = - toggleLiveReloading (enable) { + + async toggleLiveReloading (enable) { if (typeof enable === 'undefined') { enable = !this.liveReloadEvents } - if (!enable) { + if (this.liveReloadEvents) { this.liveReloadEvents.close() this.liveReloadEvents = false } else if (this.datInfo) { @@ -501,7 +502,7 @@ class View { if (!archive) return let {version} = parseDatURL(this.url) - let {checkoutFS} = beakerCore.dat.library.getArchiveCheckout(archive, version) + let {checkoutFS} = await beakerCore.dat.library.getArchiveCheckout(archive, version) this.liveReloadEvents = checkoutFS.pda.watch() let event = (this.datInfo.isOwner) ? 'changed' : 'invalidated' diff --git a/app/dat-daemon.js b/app/dat-daemon.js deleted file mode 100644 index 400068bfc1..0000000000 --- a/app/dat-daemon.js +++ /dev/null @@ -1,20 +0,0 @@ -const {join} = require('path') -const rpcAPI = require('pauls-electron-rpc') -const beakerCoreDatDaemon = require('@beaker/core/dat/daemon') - -process.on('uncaughtException', (err) => { - console.error('Uncaught exception:', err) -}) - -process.on('disconnect', () => { - process.exit() -}) - -process.once('message', firstMsg => { - beakerCoreDatDaemon.setup({ - rpcAPI, - logfilePath: join(firstMsg.userDataPath, 'dat.log') - }) - process.send({ready: true}) - console.log('dat-daemon ready') -}) \ No newline at end of file From 9fdc2360bb999c2f5e40be960c9de3cc614451bf Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 8 Aug 2019 16:20:04 -0500 Subject: [PATCH 0002/1212] Change usage of pda --- app/background-process/ui/permissions.js | 3 +-- app/package.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/background-process/ui/permissions.js b/app/background-process/ui/permissions.js index ebe19d167b..ef34b02068 100644 --- a/app/background-process/ui/permissions.js +++ b/app/background-process/ui/permissions.js @@ -4,7 +4,6 @@ import * as beakerCore from '@beaker/core' const dat = beakerCore.dat const sitedata = beakerCore.dbs.sitedata import _get from 'lodash.get' -import pda from 'pauls-dat-api' import parseDatURL from 'parse-dat-url' import * as permPromptSubwindow from './subwindows/perm-prompt' import * as viewManager from './view-manager' @@ -77,7 +76,7 @@ export async function checkLabsPerm ({perm, labApi, apiDocsUrl, sender}) { let archive = dat.library.getArchive(key) if (archive) { let {checkoutFS} = await dat.library.getArchiveCheckout(archive, urlp.version) - let manifest = await pda.readManifest(checkoutFS).catch(_ => {}) + let manifest = await checkoutFS.pda.readManifest().catch(_ => {}) let apis = _get(manifest, 'experimental.apis') if (apis && Array.isArray(apis)) { isOptedIn = apis.includes(labApi) diff --git a/app/package.json b/app/package.json index 1b1fce0886..192bb4ca4b 100644 --- a/app/package.json +++ b/app/package.json @@ -75,7 +75,6 @@ "os-name": "^2.0.1", "page-metadata-parser": "^1.1.3", "parse-dat-url": "^3.0.1", - "pauls-dat-api": "^8.0.1", "pauls-electron-rpc": "^5.0.0", "pauls-word-boundary": "^1.0.0", "pify": "^2.3.0", From 9ecb00ff920d50ca4e49656152f0f132c1f093b7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 16 Aug 2019 15:44:08 -0500 Subject: [PATCH 0003/1212] Fix watch-stream close -> destroy --- app/background-process/ui/view-manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index bc44fcb60f..af07e38f5f 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -495,7 +495,7 @@ class View { enable = !this.liveReloadEvents } if (this.liveReloadEvents) { - this.liveReloadEvents.close() + this.liveReloadEvents.destroy() this.liveReloadEvents = false } else if (this.datInfo) { let archive = beakerCore.dat.library.getArchive(this.datInfo.key) @@ -520,7 +520,7 @@ class View { stopLiveReloading () { if (this.liveReloadEvents) { - this.liveReloadEvents.close() + this.liveReloadEvents.destroy() this.liveReloadEvents = false this.emitUpdateState() } From d01b3876b6e177241f2b3a59b540525c7ab2809c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 16 Aug 2019 17:51:11 -0500 Subject: [PATCH 0004/1212] Ignore failures to inject the json renderer or error page --- app/background-process/ui/view-manager.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index af07e38f5f..a08ac1307c 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -566,7 +566,11 @@ class View { `) let jsonpath = path.join(app.getAppPath(), 'json-renderer.build.js') jsonpath = jsonpath.replace('app.asar', 'app.asar.unpacked') // fetch from unpacked dir - this.webContents.executeJavaScript(await fs.readFile(jsonpath, 'utf8')) + try { + await this.webContents.executeJavaScript(await fs.readFile(jsonpath, 'utf8')) + } catch (e) { + // ignore + } } } @@ -774,7 +778,11 @@ class View { // render failure page var errorPageHTML = errorPage(this.loadError) - this.webContents.executeJavaScript('document.documentElement.innerHTML = \'' + errorPageHTML + '\'') + try { + await this.webContents.executeJavaScript('document.documentElement.innerHTML = \'' + errorPageHTML + '\'') + } catch (e) { + // ignore + } } onUpdateTargetUrl (e, url) { From 04cc7bfe5d74f81055a673afb16e80d2b30ad8e2 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 19 Aug 2019 16:57:54 -0500 Subject: [PATCH 0005/1212] Fix: add missing async --- app/background-process/ui/view-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index a08ac1307c..b789d7d677 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -759,7 +759,7 @@ class View { this.captureScreenshot() } - onDidFailLoad (e, errorCode, errorDescription, validatedURL, isMainFrame) { + async onDidFailLoad (e, errorCode, errorDescription, validatedURL, isMainFrame) { // ignore if this is a subresource if (!isMainFrame) return From cd76cc34c0ec1c135be1ccf84a48188a15a7d551 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 19 Aug 2019 17:08:56 -0500 Subject: [PATCH 0006/1212] (Re)add edit button to location bar --- app/new-shell-window/navbar/location.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/new-shell-window/navbar/location.js b/app/new-shell-window/navbar/location.js index 710b9be1eb..518ef9921a 100644 --- a/app/new-shell-window/navbar/location.js +++ b/app/new-shell-window/navbar/location.js @@ -93,6 +93,11 @@ class NavbarLocation extends LitElement { ${this.renderLocation()} ${this.renderZoom()} ${this.renderLiveReloadingBtn()} + ${this.isDat ? html` + + ` : ''} @@ -347,6 +352,10 @@ class NavbarLocation extends LitElement { e.currentTarget.blur() } + onClickEdit (e) { + bg.views.toggleSidebar('active', 'editor') + } + onClickComments (e) { bg.views.toggleSidebar('active', 'comments') } @@ -444,7 +453,7 @@ button { button.text { width: auto; - padding: 0 6px; + padding: 0 4px; font-size: 12px; } @@ -459,6 +468,12 @@ button.text .far { font-size: 13px; } +button.text .fa-edit { + position: relative; + top: -1px; + left: 1px; +} + button.text .fa-comment-alt { font-size: 12px; } From e3fc813a2d115ac260f917df47cc56a4c2708bf7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 19 Aug 2019 17:15:49 -0500 Subject: [PATCH 0007/1212] Replace the user thumb with an icon in the navbar UI --- app/new-shell-window/navbar.js | 25 +++---------------------- app/shell-window.js | 17 ----------------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/app/new-shell-window/navbar.js b/app/new-shell-window/navbar.js index 1da65861d7..e75a461c20 100644 --- a/app/new-shell-window/navbar.js +++ b/app/new-shell-window/navbar.js @@ -14,8 +14,6 @@ class ShellWindowNavbar extends LitElement { activeTab: {type: Object}, isUpdateAvailable: {type: Boolean, attribute: 'is-update-available'}, numWatchlistNotifications: {type: Number, attribute: 'num-watchlist-notifications'}, - userUrl: {type: String, attribute: 'user-url'}, - userThumbUrl: {type: String, attribute: 'user-thumb-url'}, isUsersMenuOpen: {type: Boolean}, isBrowserMenuOpen: {type: Boolean} } @@ -208,7 +206,7 @@ class ShellWindowNavbar extends LitElement { get toggleSidebarBtn () { return html` - ` @@ -218,7 +216,7 @@ class ShellWindowNavbar extends LitElement { const cls = classMap({'users-btn': true, pressed: this.isUsersMenuOpen}) return html` ` } @@ -285,12 +283,8 @@ ShellWindowNavbar.styles = css` ${buttonResetCSS} button { - width: 30px; - position: relative; -} - -button.nav-arrow-btn { width: 28px; + position: relative; } button .fa, @@ -337,18 +331,5 @@ svg.icon.refresh { font-weight: bold; padding: 0 3px; } - -.users-btn { - margin: 0 2px; -} - -.users-btn img { - width: 24px; - height: 24px; - border-radius: 50%; - object-fit: cover; - position: relative; - top: 1px; -} ` customElements.define('shell-window-navbar', ShellWindowNavbar) diff --git a/app/shell-window.js b/app/shell-window.js index 8522f128f1..12ad6f0dfd 100644 --- a/app/shell-window.js +++ b/app/shell-window.js @@ -19,8 +19,6 @@ class ShellWindowUI extends LitElement { tabs: {type: Array}, isUpdateAvailable: {type: Boolean}, numWatchlistNotifications: {type: Number}, - userUrl: {type: String}, - userThumbUrl: {type: String}, isFullscreen: {type: Boolean} } } @@ -30,8 +28,6 @@ class ShellWindowUI extends LitElement { this.tabs = [] this.isUpdateAvailable = false this.numWatchlistNotifications = 0 - this.userUrl = '' - this.userThumbUrl = 'asset:thumb:default' this.isFullscreen = false this.activeTabIndex = -1 @@ -73,7 +69,6 @@ class ShellWindowUI extends LitElement { // listen to state updates on the auto-updater var browserEvents = fromEventStream(bg.beakerBrowser.createEventsStream()) browserEvents.addEventListener('updater-state-changed', this.onUpdaterStateChange.bind(this)) - browserEvents.addEventListener('user-thumb-changed', this.onUserThumbChanged.bind(this)) // listen to state updates on the watchlist var wlEvents = fromEventStream(bg.watchlist.createEventsStream()) @@ -87,10 +82,6 @@ class ShellWindowUI extends LitElement { this.stateHasChanged() }) this.isUpdateAvailable = bg.beakerBrowser.getInfo().updater.state === 'downloaded' - bg.beakerBrowser.getUserSession().then(user => { - this.userUrl = user.url - this.userThumbUrl = `asset:thumb:${user.url}` - }) } get activeTab () { @@ -120,8 +111,6 @@ class ShellWindowUI extends LitElement { .activeTab=${this.activeTab} ?is-update-available=${this.isUpdateAvailable} num-watchlist-notifications="${this.numWatchlistNotifications}" - user-url="${this.userUrl}" - user-thumb-url="${this.userThumbUrl}" > ` } @@ -132,12 +121,6 @@ class ShellWindowUI extends LitElement { onUpdaterStateChange (e) { this.isUpdateAvailable = (e && e.state === 'downloaded') } - - onUserThumbChanged (e) { - if (e.url === this.userUrl) { - this.userThumbUrl = `asset:thumb:${e.url}?cache=${Date.now()}` - } - } } customElements.define('shell-window', ShellWindowUI) \ No newline at end of file From 2658ce6c102b28b3bf43a4b53ff0cc8f04ae9b3c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 20 Aug 2019 09:24:53 -0500 Subject: [PATCH 0008/1212] Rework navbar (cli button, remove site-tools and move some entries to browser-menu) --- app/new-shell-window/navbar.js | 16 +++++++++------ app/new-shell-window/navbar/location.js | 26 ------------------------- app/shell-menus/browser.js | 23 +++++++++++++++++++++- app/shell-menus/site-tools.js | 1 - 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/app/new-shell-window/navbar.js b/app/new-shell-window/navbar.js index e75a461c20..973b1ebff3 100644 --- a/app/new-shell-window/navbar.js +++ b/app/new-shell-window/navbar.js @@ -83,7 +83,7 @@ class ShellWindowNavbar extends LitElement { >
${this.watchlistBtn} - ${this.toggleSidebarBtn} + ${this.openTerminalBtn} ${this.usersMenuBtn} ${this.browserMenuBtn}
@@ -204,10 +204,10 @@ class ShellWindowNavbar extends LitElement { ` } - get toggleSidebarBtn () { + get openTerminalBtn () { return html` - ` } @@ -261,8 +261,8 @@ class ShellWindowNavbar extends LitElement { this.isUsersMenuOpen = false } - onClickSidebarToggle (e) { - bg.views.toggleSidebar('active') + onClickOpenTerminal (e) { + bg.views.toggleSidebar('active', 'terminal') } async onClickBrowserMenu (e) { @@ -311,6 +311,10 @@ svg.icon.refresh { padding: 0 8px; } +button .fa-terminal { + font-size: 14px; +} + .fa-arrow-alt-circle-up { font-size: 20px; color: #67bf6b; diff --git a/app/new-shell-window/navbar/location.js b/app/new-shell-window/navbar/location.js index 518ef9921a..6a94ea12ce 100644 --- a/app/new-shell-window/navbar/location.js +++ b/app/new-shell-window/navbar/location.js @@ -28,7 +28,6 @@ class NavbarLocation extends LitElement { donateLinkHref: {type: String, attribute: 'donate-link-href'}, availableAlternative: {type: String, attribute: 'available-alternative'}, isLiveReloading: {type: Boolean, attribute: 'is-live-reloading'}, - isSiteToolsMenuOpen: {type: Boolean}, isDonateMenuOpen: {type: Boolean}, isBookmarked: {type: Boolean, attribute: 'is-bookmarked'}, isLocationFocused: {type: Boolean} @@ -50,7 +49,6 @@ class NavbarLocation extends LitElement { this.loadError = null this.donateLinkHref = false this.availableAlternative = '' - this.isSiteToolsMenuOpen = false this.isDonateMenuOpen = false this.isBookmarked = false this.isLocationFocused = false @@ -101,7 +99,6 @@ class NavbarLocation extends LitElement { - ${this.renderSiteToolsBtn()} ${this.renderAvailableAlternativeBtn()} ${this.renderDonateBtn()} ${this.renderBookmarkBtn()} @@ -207,15 +204,6 @@ class NavbarLocation extends LitElement { ` } - renderSiteToolsBtn () { - var cls = classMap({'site-tools': true, pressed: this.isSiteToolsMenuOpen}) - return html` - - ` - } - renderAvailableAlternativeBtn () { const aa = this.availableAlternative if (aa === 'dat:') { @@ -364,20 +352,6 @@ class NavbarLocation extends LitElement { bg.views.resetZoom(this.activeTabIndex) } - async onClickSiteToolsBtn (e) { - this.isSiteToolsMenuOpen = true - var rect1 = this.getClientRects()[0] - var rect2 = e.currentTarget.getClientRects()[0] - await bg.views.toggleMenu('site-tools', { - bounds: { - top: (rect1.bottom|0), - left: (rect2.right|0) - }, - params: {url: this.url} - }) - this.isSiteToolsMenuOpen = false - } - onClickAvailableAlternative (e) { var url = new URL(this.url) url.protocol = this.availableAlternative diff --git a/app/shell-menus/browser.js b/app/shell-menus/browser.js index de0960d38f..38ad3ed850 100644 --- a/app/shell-menus/browser.js +++ b/app/shell-menus/browser.js @@ -98,10 +98,20 @@ class BrowserMenu extends LitElement {
+ + + +
@@ -203,6 +213,17 @@ class BrowserMenu extends LitElement { } } + async onClickSavePage () { + var tabState = await bg.views.getTabState('active') + bg.beakerBrowser.downloadURL(tabState.url) + bg.shellMenus.close() + } + + onClickPrint () { + bg.views.print('active') + bg.shellMenus.close() + } + onClickDownloads (e) { this.shouldPersistDownloadsIndicator = false bg.shellMenus.createTab('beaker://downloads') diff --git a/app/shell-menus/site-tools.js b/app/shell-menus/site-tools.js index a8cbf5b199..e24eed4a64 100644 --- a/app/shell-menus/site-tools.js +++ b/app/shell-menus/site-tools.js @@ -2,7 +2,6 @@ import { LitElement, html, css } from '../vendor/lit-element/lit-element' import _get from 'lodash.get' import * as bg from './bg-process-rpc' -import {writeToClipboard} from '../lib/fg/event-handlers' import commonCSS from './common.css' class SiteToolsMenu extends LitElement { From e7cfcc8c02a6dc01f929b274b12468f91a0d9b3f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 20 Aug 2019 11:22:41 -0500 Subject: [PATCH 0009/1212] Rearrange context menu --- app/background-process/ui/context-menu.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/background-process/ui/context-menu.js b/app/background-process/ui/context-menu.js index 6d90b1fe67..3c0f4ddd41 100644 --- a/app/background-process/ui/context-menu.js +++ b/app/background-process/ui/context-menu.js @@ -120,6 +120,13 @@ export default function registerContextMenu () { } if (!props.linkURL && props.mediaType === 'none' && !hasText) { + menuItems.push({ + label: 'About This Site', + click: (item, win) => { + viewManager.getActive(win).toggleSidebar('site') + } + }) + menuItems.push({ type: 'separator' }) menuItems.push({ label: 'Back', enabled: webContents.canGoBack(), @@ -143,12 +150,6 @@ export default function registerContextMenu () { label: 'Print...', click: () => webContents.print() }) - menuItems.push({ - label: 'About This Site', - click: (item, win) => { - viewManager.getActive(win).toggleSidebar('site') - } - }) menuItems.push({ type: 'separator' }) } From 266b72c3fe1cc2d43f15b713c567ac3b3583c43e Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 20 Aug 2019 11:22:51 -0500 Subject: [PATCH 0010/1212] Go back to templatew for site-creation --- app/modals/create-archive.js | 46 ++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/app/modals/create-archive.js b/app/modals/create-archive.js index 05970a714a..49afad4b19 100644 --- a/app/modals/create-archive.js +++ b/app/modals/create-archive.js @@ -8,7 +8,7 @@ import inputsCSS from './inputs.css' import buttonsCSS from './buttons2.css' const BASIC_TEMPLATES = [ - {url: 'blank', title: 'No Theme', thumb: html``} + {url: 'blank', title: 'Empty Site', thumb: html``} ] class CreateArchiveModal extends LitElement { @@ -16,7 +16,7 @@ class CreateArchiveModal extends LitElement { return { title: {type: String}, description: {type: String}, - currentTheme: {type: String}, + currentTemplate: {type: String}, errors: {type: Object} } } @@ -29,8 +29,8 @@ class CreateArchiveModal extends LitElement { this.type = null this.links = null this.networked = true - this.themes = [] - this.currentTheme = 'blank' + this.templates = [] + this.currentTemplate = 'blank' this.errors = {} // export interface @@ -45,8 +45,8 @@ class CreateArchiveModal extends LitElement { this.type = params.type ? Array.isArray(params.type) ? params.type[0] : params.type : '' this.links = params.links this.networked = ('networked' in params) ? params.networked : true - this.themes = BASIC_TEMPLATES.concat( - await bg.archives.list({type: 'unwalled.garden/theme', isSaved: true}) + this.templates = BASIC_TEMPLATES.concat( + await bg.archives.list({type: 'unwalled.garden/template', isSaved: true}) ) await this.requestUpdate() } @@ -56,7 +56,7 @@ class CreateArchiveModal extends LitElement { render () { const template = (url, title, thumb) => { - const cls = classMap({template: true, selected: url === this.currentTheme}) + const cls = classMap({template: true, selected: url === this.currentTemplate}) return html`
this.onClickTemplate(e, url)}> ${thumb ? thumb : html``} @@ -72,9 +72,9 @@ class CreateArchiveModal extends LitElement {
-
-
- ${this.themes.map(t => template(t.url, t.title, t.thumb))} +
+
+ ${this.templates.map(t => template(t.url, t.title, t.thumb))}
@@ -101,7 +101,7 @@ class CreateArchiveModal extends LitElement { // = async onClickTemplate (e, url) { - this.currentTheme = url + this.currentTemplate = url await this.updateComplete this.shadowRoot.querySelector('input').focus() // focus the title input } @@ -133,7 +133,7 @@ class CreateArchiveModal extends LitElement { try { var url - if (!this.currentTheme.startsWith('dat:')) { + if (!this.currentTemplate.startsWith('dat:')) { // using builtin template url = await bg.datArchive.createArchive({ title: this.title, @@ -144,22 +144,16 @@ class CreateArchiveModal extends LitElement { prompt: false }) } else { - // using a theme - await bg.datArchive.download(this.currentTheme) - url = await bg.datArchive.createArchive({ + // using a template + await bg.datArchive.download(this.currentTemplate) + url = await bg.datArchive.forkArchive(this.currentTemplate, { title: this.title, description: this.description, - type: '', + type: [], networked: this.networked, links: this.links, prompt: false }) - // TODO mount the theme instead of copying - await bg.datArchive.exportToArchive({ - src: this.currentTheme, - dst: url + 'theme', - skipUndownloadedFiles: false - }) } this.cbs.resolve({url}) } catch (e) { @@ -194,7 +188,7 @@ hr { user-select: none; } -.layout .themes { +.layout .templates { width: 624px; } @@ -204,14 +198,14 @@ hr { padding: 20px; } -.themes { +.templates { height: 468px; overflow-y: auto; background: #fafafa; border-right: 1px solid #ddd; } -.themes-heading { +.templates-heading { margin: 20px 20px 0px; padding-bottom: 5px; border-bottom: 1px solid #ddd; @@ -219,7 +213,7 @@ hr { font-size: 11px; } -.themes-selector { +.templates-selector { display: grid; grid-gap: 20px; padding: 10px 20px; From 48415303240838a5fa19293a4f051068ce652487 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 28 Aug 2019 15:25:38 -0500 Subject: [PATCH 0011/1212] Remove getDefaultLocalPath() --- app/background-process/browser.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/app/background-process/browser.js b/app/background-process/browser.js index 7f99ce0329..3657d9758c 100644 --- a/app/background-process/browser.js +++ b/app/background-process/browser.js @@ -138,7 +138,6 @@ export const WEBAPI = { setSetting, getUserSetupStatus, setUserSetupStatus, - getDefaultLocalPath, setStartPageBackgroundImage, getDefaultProtocolSettings, setAsDefaultProtocolClient, @@ -603,24 +602,6 @@ function openFolder (folderPath) { shell.openExternal('file://' + folderPath) } -async function getDefaultLocalPath (dir, title) { - // massage the title - title = typeof title === 'string' ? title : '' - title = title.replace(INVALID_SAVE_FOLDER_CHAR_REGEX, '') - if (!title.trim()) { - title = 'Untitled' - } - title = slugify(title).toLowerCase() - - // find an available variant of title - let tryNum = 1 - let titleVariant = title - while (await jetpack.existsAsync(path.join(dir, titleVariant))) { - titleVariant = `${title}-${++tryNum}` - } - return path.join(dir, titleVariant) -} - async function doWebcontentsCmd (method, wcId, ...args) { var wc = webContents.fromId(+wcId) if (!wc) throw new Error(`WebContents not found (${wcId})`) From 80448ffc68ee32d6e69ed8c30e9b71ec06fdbb7d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 28 Aug 2019 15:27:37 -0500 Subject: [PATCH 0012/1212] Remove preview-mode tools --- .../ui/subwindows/shell-menus.js | 9 +- app/location-bar.js | 3 +- app/shell-menus.js | 3 - app/shell-menus/preview-mode-tools.js | 117 ------------------ 4 files changed, 2 insertions(+), 130 deletions(-) delete mode 100644 app/shell-menus/preview-mode-tools.js diff --git a/app/background-process/ui/subwindows/shell-menus.js b/app/background-process/ui/subwindows/shell-menus.js index 4c101f81ff..f79e179eca 100644 --- a/app/background-process/ui/subwindows/shell-menus.js +++ b/app/background-process/ui/subwindows/shell-menus.js @@ -21,7 +21,7 @@ import shellMenusRPCManifest from '../../rpc-manifests/shell-menus' // = const MARGIN_SIZE = 10 -const IS_RIGHT_ALIGNED = ['browser', 'users', 'bookmark', 'donate', 'site-tools', 'preview-mode-tools'] +const IS_RIGHT_ALIGNED = ['browser', 'users', 'bookmark', 'donate', 'site-tools'] var events = new Events() var views = {} // map of {[parentWindow.id] => BrowserView} @@ -99,13 +99,6 @@ export function reposition (parentWindow) { width: 220, height: 152 }) - } else if (view.menuId === 'preview-mode-tools') { - setBounds({ - x: parentBounds.width - view.boundsOpt.left, - y: view.boundsOpt.top, - width: 220, - height: 122 - }) } } } diff --git a/app/location-bar.js b/app/location-bar.js index e9c4aa5430..75e91dd26a 100644 --- a/app/location-bar.js +++ b/app/location-bar.js @@ -536,9 +536,8 @@ customElements.define('location-bar', LocationBar) // = const TRAILING_SLASH_REGEX = /(\/$)/ -const PREVIEW_REGEX = /(\+preview)/ function normalizeURL (str = '') { - return str.replace(TRAILING_SLASH_REGEX, '').replace(PREVIEW_REGEX, '') + return str.replace(TRAILING_SLASH_REGEX, '') } function makeSafe (str = '') { diff --git a/app/shell-menus.js b/app/shell-menus.js index 4fb786d8be..3ba8327188 100644 --- a/app/shell-menus.js +++ b/app/shell-menus.js @@ -6,7 +6,6 @@ import './shell-menus/users' import './shell-menus/bookmark' import './shell-menus/donate' import './shell-menus/site-tools' -import './shell-menus/preview-mode-tools' class MenusWrapper extends LitElement { static get properties () { @@ -84,8 +83,6 @@ class MenusWrapper extends LitElement { return html`` case 'site-tools': return html`` - case 'preview-mode-tools': - return html`` } return html`
` } diff --git a/app/shell-menus/preview-mode-tools.js b/app/shell-menus/preview-mode-tools.js deleted file mode 100644 index afbd65131b..0000000000 --- a/app/shell-menus/preview-mode-tools.js +++ /dev/null @@ -1,117 +0,0 @@ -/* globals customElements */ -import { LitElement, html, css } from '../vendor/lit-element/lit-element' -import { classMap } from '../vendor/lit-element/lit-html/directives/class-map' -import _get from 'lodash.get' -import * as bg from './bg-process-rpc' -import commonCSS from './common.css' - -class PreviewModeToolsMenu extends LitElement { - constructor () { - super() - this.reset() - } - - reset () { - this.url = null - this.datKey = null - } - - async init (params) { - this.url = params.url - this.origin = toOrigin(this.url) - this.datKey = await bg.datArchive.resolveName(this.url) - this.hasChanges = (await bg.archives.diffLocalSyncPathListing(this.datKey, {compareContent: true, shallow: true})).length > 0 - await this.requestUpdate() - } - - // rendering - // = - - render () { - const changesCls = classMap({ - 'menu-item': true, - disabled: !this.hasChanges - }) - return html` - -
- - -
-
- - Review changes -
-
- - Commit all changes -
-
- ` - } - - // events - // = - - onClickGotoPreview () { - bg.views.loadURL('active', `${this.origin}+preview`) - bg.shellMenus.close() - } - - onClickGotoLive () { - bg.views.loadURL('active', `${this.origin}`) - bg.shellMenus.close() - } - - onClickGotoReview () { - bg.views.loadURL('active', `beaker://editor/${this.origin}`) - bg.shellMenus.close() - } - - async onClickCommit () { - var url = this.url - var datKey = this.datKey - - if (!confirm('Commit all changes?')) { - bg.shellMenus.close() - return - } - - var currentDiff = await bg.archives.diffLocalSyncPathListing(datKey, {compareContent: true, shallow: true}) - var paths = fileDiffsToPaths(currentDiff) - await bg.archives.publishLocalSyncPathListing(datKey, {shallow: false, paths}) - - bg.views.loadURL('active', url) // reload the page - bg.shellMenus.close() - } -} -PreviewModeToolsMenu.styles = [commonCSS, css` -.wrapper { - padding: 4px 0; -} -`] - -customElements.define('preview-mode-tools-menu', PreviewModeToolsMenu) - -function toOrigin (url) { - try { - let urlp = new URL(url) - return `${urlp.protocol}//${urlp.hostname}`.replace('+preview', '') - } catch (e) { - return url - } -} - - -function fileDiffsToPaths (filediff) { - return filediff.map(d => { - if (d.type === 'dir') return d.path + '/' // indicate that this is a folder - return d.path - }) -} From ba79cc4db179d785753996bd9e7e9ab635a7e868 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 28 Aug 2019 15:28:00 -0500 Subject: [PATCH 0013/1212] Remove old permissioning hack --- app/background-process/ui/permissions.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/background-process/ui/permissions.js b/app/background-process/ui/permissions.js index ef34b02068..bdc822aaeb 100644 --- a/app/background-process/ui/permissions.js +++ b/app/background-process/ui/permissions.js @@ -105,14 +105,6 @@ async function onPermissionRequestHandler (webContents, permission, cb, opts) { return cb(true) } - // HACK - // until the applications system can be properly implemented, - // allow all requests from dat://beaker.social/, our default social app - // -prf - if (url.startsWith('dat://beaker.social/') && permission === 'dangerousAppControl') { - return cb(true) - } - // look up the containing window var {win, view} = getContaining(webContents) if (!win || !view) { From 16939570281efa49053f2dbd83e73c901f34f2b1 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 28 Aug 2019 15:29:13 -0500 Subject: [PATCH 0014/1212] Remove old applications tools --- app/modals.js | 3 - app/modals/bg-process-rpc.js | 2 - app/modals/install-application.js | 99 -------------------------- app/new-shell-window/bg-process-rpc.js | 2 - app/shell-menus/bg-process-rpc.js | 2 - 5 files changed, 108 deletions(-) delete mode 100644 app/modals/install-application.js diff --git a/app/modals.js b/app/modals.js index aef65381c5..e103d15386 100644 --- a/app/modals.js +++ b/app/modals.js @@ -10,7 +10,6 @@ import './modals/prompt' import './modals/basic-auth' import './modals/user' import './modals/create-user-session' -import './modals/install-application' class ModalsWrapper extends LitElement { static get properties () { @@ -82,8 +81,6 @@ class ModalsWrapper extends LitElement { return html`` case 'create-user-session': return html`` - case 'install-application': - return html`` } return html`
` } diff --git a/app/modals/bg-process-rpc.js b/app/modals/bg-process-rpc.js index 3edfcd129f..f9b5edae94 100644 --- a/app/modals/bg-process-rpc.js +++ b/app/modals/bg-process-rpc.js @@ -1,6 +1,5 @@ import * as rpc from 'pauls-electron-rpc' import browserManifest from '@beaker/core/web-apis/manifests/internal/browser' -import applicationsManifest from '@beaker/core/web-apis/manifests/internal/applications' import usersManifest from '@beaker/core/web-apis/manifests/internal/users' import archivesManifest from '@beaker/core/web-apis/manifests/internal/archives' import datArchiveManifest from '@beaker/core/web-apis/manifests/external/dat-archive' @@ -9,7 +8,6 @@ import modalsManifest from '../background-process/rpc-manifests/modals' export const beakerBrowser = rpc.importAPI('beaker-browser', browserManifest) export const users = rpc.importAPI('users', usersManifest) -export const applications = rpc.importAPI('applications', applicationsManifest) export const archives = rpc.importAPI('archives', archivesManifest) export const datArchive = rpc.importAPI('dat-archive', datArchiveManifest) export const profiles = rpc.importAPI('unwalled-garden-profiles', profilesManifest) diff --git a/app/modals/install-application.js b/app/modals/install-application.js deleted file mode 100644 index a25ba75617..0000000000 --- a/app/modals/install-application.js +++ /dev/null @@ -1,99 +0,0 @@ -/* globals customElements */ -import { LitElement, html, css } from '../vendor/lit-element/lit-element' -import { classMap } from '../vendor/lit-element/lit-html/directives/class-map' -import { repeat } from '../vendor/lit-element/lit-html/directives/repeat' -import { ucfirst } from '../lib/strings' -import * as bg from './bg-process-rpc' -import commonCSS from './common.css' -import inputsCSS from './inputs.css' -import buttonsCSS from './buttons2.css' - -class InstallApplicationModal extends LitElement { - constructor () { - super() - this.cbs = null - this.url = '' - this.appInfo = null - } - - async init (params, cbs) { - this.cbs = cbs - this.url = params.url - this.appInfo = await bg.applications.getInfo(params.url) - console.log(this.appInfo) - await this.requestUpdate() - } - - // rendering - // = - - render () { - if (!this.appInfo) return html`
` - return html` -
-

Install ${this.appInfo.title || 'application'}

- - - ${this.appInfo.description ? html`

${this.appInfo.description}

` : ''} - - ${this.appInfo.permissions.length > 0 ? html` -

Permissions:

-
    - ${repeat(this.appInfo.permissions, perm => html` -
  • ${perm.description}
  • - `)} -
- ` : ''} - -
- - -
- -
- ` - } - - // event handlers - // = - - updated () { - // adjust height based on rendering - var height = this.shadowRoot.querySelector('div').clientHeight - bg.modals.resizeSelf({height}) - } - - onClickCancel (e) { - e.preventDefault() - this.cbs.reject(new Error('Canceled')) - } - - async onSubmit (e) { - e.preventDefault() - - try { - await bg.applications.install(this.url) - this.cbs.resolve(true) - } catch (e) { - this.cbs.reject(e.message || e.toString()) - } - } -} -InstallApplicationModal.styles = [commonCSS, inputsCSS, buttonsCSS, css` -.wrapper { - padding: 10px 20px 16px; -} - -form { - padding: 0; - margin: 0; -} - -h3 { - margin: 1em 0; - font-weight: 600; -} - -`] - -customElements.define('install-application-modal', InstallApplicationModal) \ No newline at end of file diff --git a/app/new-shell-window/bg-process-rpc.js b/app/new-shell-window/bg-process-rpc.js index e59b7fbaaa..c7b4f22b5a 100644 --- a/app/new-shell-window/bg-process-rpc.js +++ b/app/new-shell-window/bg-process-rpc.js @@ -1,13 +1,11 @@ import * as rpc from 'pauls-electron-rpc' import browserManifest from '@beaker/core/web-apis/manifests/internal/browser' -import applicationsManifest from '@beaker/core/web-apis/manifests/internal/applications' import bookmarksManifest from '@beaker/core/web-apis/manifests/external/bookmarks' import watchlistManifest from '@beaker/core/web-apis/manifests/internal/watchlist' import viewsManifest from '../background-process/rpc-manifests/views' import datArchiveManifest from '@beaker/core/web-apis/manifests/external/dat-archive' export const beakerBrowser = rpc.importAPI('beaker-browser', browserManifest) -export const applications = rpc.importAPI('applications', applicationsManifest) export const bookmarks = rpc.importAPI('bookmarks', bookmarksManifest) export const watchlist = rpc.importAPI('watchlist', watchlistManifest) export const views = rpc.importAPI('background-process-views', viewsManifest) diff --git a/app/shell-menus/bg-process-rpc.js b/app/shell-menus/bg-process-rpc.js index b78abb1bb5..3e63363b66 100644 --- a/app/shell-menus/bg-process-rpc.js +++ b/app/shell-menus/bg-process-rpc.js @@ -1,7 +1,6 @@ import * as rpc from 'pauls-electron-rpc' import browserManifest from '@beaker/core/web-apis/manifests/internal/browser' import usersManifest from '@beaker/core/web-apis/manifests/internal/users' -import applicationsManifest from '@beaker/core/web-apis/manifests/internal/applications' import archivesManifest from '@beaker/core/web-apis/manifests/internal/archives' import bookmarksManifest from '@beaker/core/web-apis/manifests/external/bookmarks' import historyManifest from '@beaker/core/web-apis/manifests/internal/history' @@ -15,7 +14,6 @@ import followsManifest from '@beaker/core/web-apis/manifests/external/unwalled-g export const beakerBrowser = rpc.importAPI('beaker-browser', browserManifest) export const users = rpc.importAPI('users', usersManifest) -export const applications = rpc.importAPI('applications', applicationsManifest) export const archives = rpc.importAPI('archives', archivesManifest) export const bookmarks = rpc.importAPI('bookmarks', bookmarksManifest) export const history = rpc.importAPI('history', historyManifest) From 0d4bf8b75423bfeaec19dc6be06b09e45b46fcb9 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 28 Aug 2019 15:35:11 -0500 Subject: [PATCH 0015/1212] Update to new beaker-core filesystem arch --- app/background-process.js | 2 +- app/background-process/browser.js | 5 +- .../protocols/beaker-favicon.js | 2 +- app/background-process/ui/context-menu.js | 2 +- app/background-process/ui/permissions.js | 4 +- app/background-process/ui/view-manager.js | 56 +++++++++---------- app/modals/select-file.js | 2 +- 7 files changed, 35 insertions(+), 38 deletions(-) diff --git a/app/background-process.js b/app/background-process.js index 82c1920742..7723b5a28c 100644 --- a/app/background-process.js +++ b/app/background-process.js @@ -141,7 +141,7 @@ app.on('quit', () => { app.on('custom-ready-to-show', () => { // our first window is ready to show, do any additional setup - beakerCore.dat.library.loadSavedArchives() + beakerCore.dat.archives.loadSavedArchives() }) // only run one instance diff --git a/app/background-process/browser.js b/app/background-process/browser.js index 3657d9758c..e7593b4eec 100644 --- a/app/background-process/browser.js +++ b/app/background-process/browser.js @@ -91,13 +91,12 @@ export async function setup () { // create a new user if none exists var defaultUser = await beakerCore.users.getDefault() if (!defaultUser) { - let newUserUrl = await beakerCore.dat.library.createNewArchive({ + let archive = await beakerCore.dat.archives.createNewArchive({ title: 'Anonymous', type: 'unwalled.garden/person' }) - let archive = beakerCore.dat.library.getArchive(newUserUrl) await archive.pda.writeFile('/thumb.jpg', await jetpack.cwd(__dirname).cwd('assets/img').readAsync('default-user-thumb.jpg', 'buffer'), 'binary') - await beakerCore.users.add('anonymous', newUserUrl, true) + await beakerCore.users.add('anonymous', archive.url, true) } // wire up events diff --git a/app/background-process/protocols/beaker-favicon.js b/app/background-process/protocols/beaker-favicon.js index 7418cea8e3..1d73d46e9a 100644 --- a/app/background-process/protocols/beaker-favicon.js +++ b/app/background-process/protocols/beaker-favicon.js @@ -50,7 +50,7 @@ export function setup () { let datResolvedUrl = url if (url.startsWith('dat://')) { datResolvedUrl = await dat.dns.resolveName(url) - datfs = dat.library.getArchive(datResolvedUrl) // (only try if the dat is loaded) + datfs = dat.archives.getArchive(datResolvedUrl) // (only try if the dat is loaded) } if (datfs) { // try .ico diff --git a/app/background-process/ui/context-menu.js b/app/background-process/ui/context-menu.js index 3c0f4ddd41..690d8742c9 100644 --- a/app/background-process/ui/context-menu.js +++ b/app/background-process/ui/context-menu.js @@ -18,7 +18,7 @@ export default function registerContextMenu () { // var isOwner = false // if (isDat) { // let key = await beakerCore.dat.dns.resolveName(props.pageURL) - // let archive = beakerCore.dat.library.getArchive(key) + // let archive = beakerCore.dat.archives.getArchive(key) // isOwner = archive && archive.writable // } diff --git a/app/background-process/ui/permissions.js b/app/background-process/ui/permissions.js index bdc822aaeb..05dea3fa0d 100644 --- a/app/background-process/ui/permissions.js +++ b/app/background-process/ui/permissions.js @@ -73,9 +73,9 @@ export async function checkLabsPerm ({perm, labApi, apiDocsUrl, sender}) { // check dat.json for opt-in let isOptedIn = false - let archive = dat.library.getArchive(key) + let archive = dat.archives.getArchive(key) if (archive) { - let {checkoutFS} = await dat.library.getArchiveCheckout(archive, urlp.version) + let {checkoutFS} = await dat.archives.getArchiveCheckout(archive, urlp.version) let manifest = await checkoutFS.pda.readManifest().catch(_ => {}) let apis = _get(manifest, 'experimental.apis') if (apis && Array.isArray(apis)) { diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index b789d7d677..de9d67a11b 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -498,11 +498,11 @@ class View { this.liveReloadEvents.destroy() this.liveReloadEvents = false } else if (this.datInfo) { - let archive = beakerCore.dat.library.getArchive(this.datInfo.key) + let archive = beakerCore.dat.archives.getArchive(this.datInfo.key) if (!archive) return let {version} = parseDatURL(this.url) - let {checkoutFS} = await beakerCore.dat.library.getArchiveCheckout(archive, version) + let {checkoutFS} = await beakerCore.dat.archives.getArchiveCheckout(archive, version) this.liveReloadEvents = checkoutFS.pda.watch() let event = (this.datInfo.isOwner) ? 'changed' : 'invalidated' @@ -605,11 +605,11 @@ class View { this.numComments = 0 var userSession = getUserSessionFor(this.browserWindow.webContents) - var followedUsers = (await beakerCore.crawler.follows.list({filters: {authors: userSession.url}})).map(({topic}) => topic.url) + var followedUsers = (await beakerCore.uwg.follows.list({filters: {authors: userSession.url}})).map(({topic}) => topic.url) var authors = [userSession.url].concat(followedUsers) // TODO replace with native 'count' method - var cs = await beakerCore.crawler.comments.thread(this.url.replace('+preview', ''), {filters: {authors}}) + var cs = await beakerCore.uwg.comments.thread(this.url, {filters: {authors}}) function countComments (comments) { return comments.reduce((acc, comment) => acc + 1 + (comment.replies ? countComments(comment.replies) : 0), 0) } @@ -634,7 +634,7 @@ class View { // fetch new state try { var key = await beakerCore.dat.dns.resolveName(this.url) - this.datInfo = await beakerCore.dat.library.getArchiveInfo(key) + this.datInfo = await beakerCore.dat.archives.getArchiveInfo(key) this.peers = this.datInfo.peers this.donateLinkHref = _get(this, 'datInfo.links.payment.0.href') } catch (e) { @@ -642,9 +642,9 @@ class View { } if (this.datInfo) { let userSession = getUserSessionFor(this.browserWindow.webContents) - let userFollows = await beakerCore.crawler.follows.list({filters: {authors: userSession.url}}) + let userFollows = await beakerCore.uwg.follows.list({filters: {authors: userSession.url}}) let followAuthors = [userSession.url].concat(userFollows.map(f => f.topic.url)) - let siteFollowers = await beakerCore.crawler.follows.list({filters: {topics: this.datInfo.url, authors: followAuthors}}) + let siteFollowers = await beakerCore.uwg.follows.list({filters: {topics: this.datInfo.url, authors: followAuthors}}) this.numFollowers = siteFollowers.length } if (!noEmit) this.emitUpdateState() @@ -854,27 +854,25 @@ export function setup () { } } } - beakerCore.dat.library.createEventStream().on('data', ([evt, {details}]) => { - if (evt === 'updated') { - iterateViews(view => { - if (view.datInfo && view.datInfo.url === details.url) { - view.refreshState() - } - }) - } - if (evt === 'network-changed') { - iterateViews(view => { - if (view.datInfo && view.datInfo.url === details.url) { - // update peer count - view.peers = details.connections - view.emitUpdateState() - } - if (view.wasDatTimeout && view.url.startsWith(details.url)) { - // refresh if this was a timed-out dat site (peers have been found) - view.webContents.reload() - } - }) - } + beakerCore.dat.archives.on('updated', ({details}) => { + iterateViews(view => { + if (view.datInfo && view.datInfo.url === details.url) { + view.refreshState() + } + }) + }) + beakerCore.dat.archives.on('network-changed', ({details}) => { + iterateViews(view => { + if (view.datInfo && view.datInfo.url === details.url) { + // update peer count + view.peers = details.connections + view.emitUpdateState() + } + if (view.wasDatTimeout && view.url.startsWith(details.url)) { + // refresh if this was a timed-out dat site (peers have been found) + view.webContents.reload() + } + }) }) } @@ -1255,7 +1253,7 @@ rpc.exportAPI('background-process-views', viewsRPCManifest, { var win = getWindow(this.sender) var view = getByIndex(win, tab) if (view && view.datInfo) { - var networkStats = await beakerCore.dat.library.getArchiveNetworkStats(view.datInfo.key) + var networkStats = await beakerCore.dat.archives.getArchiveNetworkStats(view.datInfo.key) return { peers: view.peers, networkStats diff --git a/app/modals/select-file.js b/app/modals/select-file.js index 9774974621..e115cfe85a 100644 --- a/app/modals/select-file.js +++ b/app/modals/select-file.js @@ -119,7 +119,7 @@ class SelectFileModal extends LitElement { } async readdir () { - var files = await bg.datArchive.readdir(this.archive, this.path, {stat: true}) + var files = await bg.datArchive.readdir(this.archive, this.path, {stat: true}) files.forEach(file => { file.stat = new Stat(file.stat) file.path = joinPath(this.path, file.name) From 06f0cbb6fb6e547d65327093a4cd1fbea2f79d9b Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 28 Aug 2019 15:35:56 -0500 Subject: [PATCH 0016/1212] Formatter --- app/background-process/protocols/asset.js | 2 +- app/background-process/ui/view-manager.js | 3 +- app/builtin-pages/com/editor/home.js | 6 +-- app/builtin-pages/com/editor/settings-form.js | 4 +- app/builtin-pages/views/editor.js | 6 +-- app/builtin-pages/views/settings.js | 1 - app/lib/bg/image.js | 2 +- app/lib/fg/anchor-markdown-header.js | 50 +++++++++---------- app/lib/fg/markdown.js | 1 - app/location-bar.js | 2 +- app/modals/prompt.js | 1 - app/new-shell-window/navbar/location.js | 1 - app/prompts/create-page.js | 2 +- 13 files changed, 37 insertions(+), 44 deletions(-) diff --git a/app/background-process/protocols/asset.js b/app/background-process/protocols/asset.js index 200fa762ec..37002b7d9d 100644 --- a/app/background-process/protocols/asset.js +++ b/app/background-process/protocols/asset.js @@ -3,7 +3,7 @@ * * Helper protocol to serve site favicons and avatars from the cache. * Examples: - * + * * - asset:favicon:dat://beakerbrowser.com * - asset:favicon-32:dat://beakerbrowser.com * - asset:thumb:dat://beakerbrowser.com diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index de9d67a11b..a98f6bee01 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -376,7 +376,7 @@ class View { } } this.resize() - + // emit this.emitUpdateState() } @@ -489,7 +489,6 @@ class View { // live reloading // = - async toggleLiveReloading (enable) { if (typeof enable === 'undefined') { enable = !this.liveReloadEvents diff --git a/app/builtin-pages/com/editor/home.js b/app/builtin-pages/com/editor/home.js index abf9deaa92..4df7227e2a 100644 --- a/app/builtin-pages/com/editor/home.js +++ b/app/builtin-pages/com/editor/home.js @@ -141,11 +141,11 @@ function renderReadme (archiveInfo, readmeMd) { highlight (str, lang) { if (lang && hljs.getLanguage(lang)) { try { - return hljs.highlight(lang, str).value; + return hljs.highlight(lang, str).value } catch (__) {} } - - return ''; // use external default escaping + + return '' // use external default escaping } }) diff --git a/app/builtin-pages/com/editor/settings-form.js b/app/builtin-pages/com/editor/settings-form.js index 44ea9e8973..2ad21d9b07 100644 --- a/app/builtin-pages/com/editor/settings-form.js +++ b/app/builtin-pages/com/editor/settings-form.js @@ -519,7 +519,7 @@ async function onSubmitSettings (e, workingCheckout, workingDatJson) { workingDatJson.application.permissions[perm] = Array.from(capset) } } - + // write manifest await workingCheckout.writeFile('/dat.json', JSON.stringify(workingDatJson, null, 2)) @@ -545,7 +545,7 @@ function onToggleExpanded (id) { document.querySelector(`section#${expandedSection}`).classList.add('collapsed') } catch (e) { /* ignore */ } } - + if (expandedSection === id) expandedSection = false else expandedSection = id sessionStorage.expandedSection = expandedSection diff --git a/app/builtin-pages/views/editor.js b/app/builtin-pages/views/editor.js index 854a00daa3..8f96880257 100644 --- a/app/builtin-pages/views/editor.js +++ b/app/builtin-pages/views/editor.js @@ -450,7 +450,7 @@ function update () {
${settingsForm.render(workingCheckout, isReadonly, archive.info, workingDatJson)} -
`, +
` ) } else { yo.update( @@ -468,7 +468,7 @@ function update () {
-
`, + ` ) } yo.update( @@ -1057,7 +1057,7 @@ function onClickArchiveMenu (e) { _get(archive, 'info.userSettings.isSaved') ? {icon: 'fas fa-trash', label: 'Remove from my websites', click: onArchiveUnsave} : {icon: 'fas fa-save', label: 'Add to my websites', click: onArchiveSave}, - {icon: 'link', label: 'Copy link', click: () => {writeToClipboard(archive.url); toast.create('Link copied to clipboard')}}, + {icon: 'link', label: 'Copy link', click: () => { writeToClipboard(archive.url); toast.create('Link copied to clipboard') }}, {icon: 'far fa-clone', label: 'Duplicate this site', click: onFork} ] }) diff --git a/app/builtin-pages/views/settings.js b/app/builtin-pages/views/settings.js index 906b1cdda0..15877290b3 100644 --- a/app/builtin-pages/views/settings.js +++ b/app/builtin-pages/views/settings.js @@ -22,7 +22,6 @@ var logger = new Logger() var datCache = new DatCache() var crawlerStatus = new CrawlerStatus() - // main // = diff --git a/app/lib/bg/image.js b/app/lib/bg/image.js index 137acdfe64..ba425e4dbd 100644 --- a/app/lib/bg/image.js +++ b/app/lib/bg/image.js @@ -2,7 +2,7 @@ * @description * Takes in a bitmap buffer (from NativeImage) and finds the dimensions * of the actual image content, effectively trimming all whitespace. - * + * * @param {Buffer} buf * @param {Object} size * @param {number} size.width diff --git a/app/lib/fg/anchor-markdown-header.js b/app/lib/fg/anchor-markdown-header.js index 4eabe97480..fe521129b4 100644 --- a/app/lib/fg/anchor-markdown-header.js +++ b/app/lib/fg/anchor-markdown-header.js @@ -1,7 +1,7 @@ /** https://github.com/thlorenz/anchor-markdown-header -Copyright 2013 Thorsten Lorenz. +Copyright 2013 Thorsten Lorenz. All rights reserved. Permission is hereby granted, free of charge, to any person @@ -26,32 +26,30 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import emojiRegex from 'emoji-regex'; +import emojiRegex from 'emoji-regex' -function basicGithubId(text) { - return text.replace(/ /g,'-') +function basicGithubId (text) { + return text.replace(/ /g, '-') // escape codes .replace(/%([abcdef]|\d){2,2}/ig, '') // single chars that are removed - .replace(/[\/?!:\[\]`.,()*"';{}+=<>~\$|#@&–—]/g,'') + .replace(/[\/?!:\[\]`.,()*"';{}+=<>~\$|#@&–—]/g, '') // CJK punctuations that are removed .replace(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/g, '') - ; - } -function getGithubId(text, repetition) { - text = basicGithubId(text); +function getGithubId (text, repetition) { + text = basicGithubId(text) // If no repetition, or if the repetition is 0 then ignore. Otherwise append '-' and the number. if (repetition) { - text += '-' + repetition; + text += '-' + repetition } // Strip emojis text = text.replace(emojiRegex(), '') - return text; + return text } /** @@ -63,33 +61,33 @@ function getGithubId(text, repetition) { * @param repetition {Number} The nth occurrence of this header text, starting with 0. Not required for the 0th instance. * @return {String} The header anchor id */ -export default function anchorMarkdownHeader(header, repetition) { - var replace; - var customEncodeURI = encodeURI; +export default function anchorMarkdownHeader (header, repetition) { + var replace + var customEncodeURI = encodeURI - replace = getGithubId; - customEncodeURI = function(uri) { - var newURI = encodeURI(uri); + replace = getGithubId + customEncodeURI = function (uri) { + var newURI = encodeURI(uri) // encodeURI replaces the zero width joiner character // (used to generate emoji sequences, e.g.Female Construction Worker 👷🏼‍♀️) // github doesn't URL encode them, so we replace them after url encoding to preserve the zwj character. - return newURI.replace(/%E2%80%8D/g, '\u200D'); - }; + return newURI.replace(/%E2%80%8D/g, '\u200D') + } - function asciiOnlyToLowerCase(input) { - var result = ''; + function asciiOnlyToLowerCase (input) { + var result = '' for (var i = 0; i < input.length; ++i) { if (input[i] >= 'A' && input[i] <= 'Z') { - result += input[i].toLowerCase(); + result += input[i].toLowerCase() } else { - result += input[i]; + result += input[i] } } - return result; + return result } - var href = replace(asciiOnlyToLowerCase(header.trim()), repetition); + var href = replace(asciiOnlyToLowerCase(header.trim()), repetition) - return customEncodeURI(href); + return customEncodeURI(href) }; \ No newline at end of file diff --git a/app/lib/fg/markdown.js b/app/lib/fg/markdown.js index 5c06391dee..9fbe76b02f 100644 --- a/app/lib/fg/markdown.js +++ b/app/lib/fg/markdown.js @@ -61,4 +61,3 @@ export default function create ({allowHTML, useHeadingIds, useHeadingAnchors, hr return md } - diff --git a/app/location-bar.js b/app/location-bar.js index 75e91dd26a..5f2af07bd9 100644 --- a/app/location-bar.js +++ b/app/location-bar.js @@ -45,7 +45,7 @@ class LocationBar extends LitElement { if (platform === 'win32') { document.body.classList.add('win32') } - + // disallow right click window.addEventListener('contextmenu', e => e.preventDefault()) diff --git a/app/modals/prompt.js b/app/modals/prompt.js index 66470d594d..3fc0ed3018 100644 --- a/app/modals/prompt.js +++ b/app/modals/prompt.js @@ -26,7 +26,6 @@ class PromptModal extends LitElement { bg.modals.resizeSelf({width, height}) } - firstUpdated () { this.shadowRoot.querySelector('input').focus() } diff --git a/app/new-shell-window/navbar/location.js b/app/new-shell-window/navbar/location.js index 6a94ea12ce..8852e52c8a 100644 --- a/app/new-shell-window/navbar/location.js +++ b/app/new-shell-window/navbar/location.js @@ -408,7 +408,6 @@ class NavbarLocation extends LitElement { params: {url: this.url, bookmarkIsNew} }) } - } NavbarLocation.styles = [buttonResetCSS, css` :host { diff --git a/app/prompts/create-page.js b/app/prompts/create-page.js index 863f1328c2..6d28a9237b 100644 --- a/app/prompts/create-page.js +++ b/app/prompts/create-page.js @@ -78,7 +78,7 @@ class CreatePagePrompt extends LitElement { // ignore, dir already exists (probably) } } - + // create the file await bg.datArchive.writeFile(urlp.hostname, path, '') bg.prompts.openSidebar('editor') From dd84ce57494e84e5865e9944af8b712cd7dca374 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 28 Aug 2019 15:36:16 -0500 Subject: [PATCH 0017/1212] Catch potential errors thrown when capturing page screenshots --- app/background-process/ui/view-manager.js | 57 ++++++++++++----------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index a98f6bee01..c8cd65689f 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -396,34 +396,39 @@ class View { async captureScreenshot () { // capture screenshot on the root page of dat & http sites var urlp = parseDatURL(this.url) - if (['dat:', 'http:', 'https:'].includes(urlp.protocol) && urlp.pathname === '/') { - // wait a sec to allow loading to finish - await new Promise(r => setTimeout(r, 1e3)) - - // capture the page - var image = await this.browserView.webContents.capturePage() - var orgsize = image.getSize() - var bounds = findImageBounds(image.toBitmap(), orgsize) - - // adjust the bounds to match the 100x80 aspect ratio - if (bounds.width < bounds.height) { - // adjust width - bounds.right = bounds.left + (bounds.height / .8)|0 - } else { - // adjust height - bounds.bottom = bounds.top + (bounds.width * .8)|0 - } + if (['dat:', 'http:', 'https:'].includes(urlp.protocol) && urlp.pathname === '/') { + try { + // wait a sec to allow loading to finish + await new Promise(r => setTimeout(r, 1e3)) + + // capture the page + var image = await this.browserView.webContents.capturePage() + var orgsize = image.getSize() + var bounds = findImageBounds(image.toBitmap(), orgsize) + + // adjust the bounds to match the 100x80 aspect ratio + if (bounds.width < bounds.height) { + // adjust width + bounds.right = bounds.left + (bounds.height / 0.8)|0 + } else { + // adjust height + bounds.bottom = bounds.top + (bounds.width * 0.8)|0 + } - // give some margin - bounds.left = Math.max(0, bounds.left - 20) - bounds.right = Math.min(orgsize.width, bounds.right + 20) - bounds.top = Math.max(0, bounds.top - 20) - bounds.bottom = Math.min(orgsize.height, bounds.bottom + 20) + // give some margin + bounds.left = Math.max(0, bounds.left - 20) + bounds.right = Math.min(orgsize.width, bounds.right + 20) + bounds.top = Math.max(0, bounds.top - 20) + bounds.bottom = Math.min(orgsize.height, bounds.bottom + 20) - image = image - .crop(bounds) - .resize({width: 200, height: 160}) - await sitedataDb.set(this.url, 'screenshot', image.toDataURL()) + image = image + .crop(bounds) + .resize({width: 200, height: 160}) + await sitedataDb.set(this.url, 'screenshot', image.toDataURL()) + } catch (e) { + // ignore, can happen if the view was closed during wait + console.log('Failed to capture page screenshot', e) + } } } From 874749724fa6f3b432e52653a52c0cd759b2209e Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 29 Aug 2019 09:34:14 -0500 Subject: [PATCH 0018/1212] Remove some old code --- app/background-process/browser.js | 5 ----- app/background-process/protocols/beaker-favicon.js | 3 --- app/builtin-pages/views/settings.js | 2 -- 3 files changed, 10 deletions(-) diff --git a/app/background-process/browser.js b/app/background-process/browser.js index e7593b4eec..42f0587b47 100644 --- a/app/background-process/browser.js +++ b/app/background-process/browser.js @@ -101,7 +101,6 @@ export async function setup () { // wire up events app.on('web-contents-created', onWebContentsCreated) - beakerCore.users.on('user-thumb-changed', onUserThumbChanged) // window.prompt handling // - we have use ipc directly instead of using rpc, because we need custom @@ -677,10 +676,6 @@ function onWebContentsCreated (e, webContents) { }) } -function onUserThumbChanged (user) { - browserEvents.emit('user-thumb-changed', user) -} - function onWillPreventUnload (e) { var choice = dialog.showMessageBox({ type: 'question', diff --git a/app/background-process/protocols/beaker-favicon.js b/app/background-process/protocols/beaker-favicon.js index 1d73d46e9a..94c28b298f 100644 --- a/app/background-process/protocols/beaker-favicon.js +++ b/app/background-process/protocols/beaker-favicon.js @@ -35,9 +35,6 @@ export function setup () { // if beaker://, pull from assets if (url.startsWith('beaker://')) { let name = /beaker:\/\/([^\/]+)/.exec(url)[1] - if (url.startsWith('beaker://library/?view=addressbook')) name = 'addressbook' - if (url.startsWith('beaker://library/?view=bookmarks')) name = 'bookmarks' - if (url.startsWith('beaker://library/?view=websites')) name = 'websites' return fs.readFile(path.join(__dirname, `./assets/img/favicons/${name}.png`), (err, buf) => { cb({mimeType: 'image/png', data: buf || defaultFaviconBuffer}) }) diff --git a/app/builtin-pages/views/settings.js b/app/builtin-pages/views/settings.js index 15877290b3..139cd57f66 100644 --- a/app/builtin-pages/views/settings.js +++ b/app/builtin-pages/views/settings.js @@ -7,8 +7,6 @@ import DatCache from '../com/settings/dat-cache' import CrawlerStatus from '../com/settings/crawler-status' import renderBuiltinPagesNav from '../com/builtin-pages-nav' -const DEFAULT_APP_NAMES = ['start', 'feed', 'profile', 'library', 'bookmarks', 'search'] - // globals // = From c2c97388eb4b5d44d2b5187cc3309d5b622aca75 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 29 Aug 2019 09:35:17 -0500 Subject: [PATCH 0019/1212] Move to uwg bookmarks --- app/background-process/ui/view-manager.js | 7 ++++--- app/location-bar.js | 7 +++---- app/new-shell-window/bg-process-rpc.js | 4 ++-- app/new-shell-window/navbar/location.js | 3 ++- app/shell-menus/bg-process-rpc.js | 4 ++-- app/shell-menus/bookmark.js | 24 +++++------------------ 6 files changed, 18 insertions(+), 31 deletions(-) diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index c8cd65689f..af0ce65e92 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -31,7 +31,7 @@ import { ucfirst } from '../../lib/strings' const sitedataDb = beakerCore.dbs.sitedata const settingsDb = beakerCore.dbs.settings const historyDb = beakerCore.dbs.history -const bookmarksDb = beakerCore.dbs.bookmarks +const bookmarksAPI = beakerCore.uwg.bookmarks const ERR_ABORTED = -3 const ERR_CONNECTION_REFUSED = -102 @@ -593,12 +593,13 @@ class View { } async fetchIsBookmarked (noEmit = false) { - var bookmark = await bookmarksDb.getBookmark(0, normalizeURL(this.url, { + var url = normalizeURL(this.url, { stripFragment: false, stripWWW: false, removeQueryParameters: false, removeTrailingSlash: true - })) + }) + var bookmark = await bookmarksAPI.getOwnBookmarkByHref(getUserSessionFor(this.browserWindow.webContents), url) this.isBookmarked = !!bookmark if (!noEmit) { this.emitUpdateState() diff --git a/app/location-bar.js b/app/location-bar.js index 5f2af07bd9..3f30f34fec 100644 --- a/app/location-bar.js +++ b/app/location-bar.js @@ -7,7 +7,7 @@ import { unsafeHTML } from './vendor/lit-element/lit-html/directives/unsafe-html import { examineLocationInput } from './lib/urls' import _uniqWith from 'lodash.uniqwith' import browserManifest from '@beaker/core/web-apis/manifests/internal/browser' -import bookmarksManifest from '@beaker/core/web-apis/manifests/external/bookmarks' +import bookmarksManifest from '@beaker/core/web-apis/manifests/external/unwalled-garden-bookmarks' import historyManifest from '@beaker/core/web-apis/manifests/internal/history' import searchManifest from '@beaker/core/web-apis/manifests/external/search' import locationBarManifest from './background-process/rpc-manifests/location-bar' @@ -15,7 +15,7 @@ import viewsManifest from './background-process/rpc-manifests/views' const bg = { beakerBrowser: rpc.importAPI('beaker-browser', browserManifest), - bookmarks: rpc.importAPI('bookmarks', bookmarksManifest), + bookmarks: rpc.importAPI('unwalled-garden-bookmarks', bookmarksManifest), history: rpc.importAPI('history', historyManifest), search: rpc.importAPI('search', searchManifest), locationBar: rpc.importAPI('background-process-location-bar', locationBarManifest), @@ -166,8 +166,7 @@ class LocationBar extends LitElement { ` } if (r.record && r.record.type === 'unwalled.garden/bookmark') { - let isAuthorYou = r.record.author.url === this.userUrl - let authorTitle = isAuthorYou ? html`you`: (r.record.author.title || 'Anonymous') + let authorTitle = r.record.author.isOwner ? html`you`: (r.record.author.title || 'Anonymous') return html`
diff --git a/app/new-shell-window/bg-process-rpc.js b/app/new-shell-window/bg-process-rpc.js index c7b4f22b5a..2ad14eeacb 100644 --- a/app/new-shell-window/bg-process-rpc.js +++ b/app/new-shell-window/bg-process-rpc.js @@ -1,12 +1,12 @@ import * as rpc from 'pauls-electron-rpc' import browserManifest from '@beaker/core/web-apis/manifests/internal/browser' -import bookmarksManifest from '@beaker/core/web-apis/manifests/external/bookmarks' +import bookmarksManifest from '@beaker/core/web-apis/manifests/external/unwalled-garden-bookmarks' import watchlistManifest from '@beaker/core/web-apis/manifests/internal/watchlist' import viewsManifest from '../background-process/rpc-manifests/views' import datArchiveManifest from '@beaker/core/web-apis/manifests/external/dat-archive' export const beakerBrowser = rpc.importAPI('beaker-browser', browserManifest) -export const bookmarks = rpc.importAPI('bookmarks', bookmarksManifest) +export const bookmarks = rpc.importAPI('unwalled-garden-bookmarks', bookmarksManifest) export const watchlist = rpc.importAPI('watchlist', watchlistManifest) export const views = rpc.importAPI('background-process-views', viewsManifest) export const datArchive = rpc.importAPI('dat-archive', datArchiveManifest) \ No newline at end of file diff --git a/app/new-shell-window/navbar/location.js b/app/new-shell-window/navbar/location.js index 8852e52c8a..85307fddf4 100644 --- a/app/new-shell-window/navbar/location.js +++ b/app/new-shell-window/navbar/location.js @@ -394,7 +394,8 @@ class NavbarLocation extends LitElement { href: this.url, title: metadata.title || this.title || '', description: metadata.description || '', - tags: metadata.keywords + tags: metadata.keywords, + visibility: 'private' }) bg.views.refreshState(this.activeTabIndex) // pull latest state } diff --git a/app/shell-menus/bg-process-rpc.js b/app/shell-menus/bg-process-rpc.js index 3e63363b66..bc8afc97e2 100644 --- a/app/shell-menus/bg-process-rpc.js +++ b/app/shell-menus/bg-process-rpc.js @@ -2,7 +2,7 @@ import * as rpc from 'pauls-electron-rpc' import browserManifest from '@beaker/core/web-apis/manifests/internal/browser' import usersManifest from '@beaker/core/web-apis/manifests/internal/users' import archivesManifest from '@beaker/core/web-apis/manifests/internal/archives' -import bookmarksManifest from '@beaker/core/web-apis/manifests/external/bookmarks' +import bookmarksManifest from '@beaker/core/web-apis/manifests/external/unwalled-garden-bookmarks' import historyManifest from '@beaker/core/web-apis/manifests/internal/history' import sitedataManifest from '@beaker/core/web-apis/manifests/internal/sitedata' import downloadsManifest from '@beaker/core/web-apis/manifests/internal/downloads' @@ -15,7 +15,7 @@ import followsManifest from '@beaker/core/web-apis/manifests/external/unwalled-g export const beakerBrowser = rpc.importAPI('beaker-browser', browserManifest) export const users = rpc.importAPI('users', usersManifest) export const archives = rpc.importAPI('archives', archivesManifest) -export const bookmarks = rpc.importAPI('bookmarks', bookmarksManifest) +export const bookmarks = rpc.importAPI('unwalled-garden-bookmarks', bookmarksManifest) export const history = rpc.importAPI('history', historyManifest) export const sitedata = rpc.importAPI('sitedata', sitedataManifest) export const downloads = rpc.importAPI('downloads', downloadsManifest) diff --git a/app/shell-menus/bookmark.js b/app/shell-menus/bookmark.js index 815b5c2254..052efe5e0a 100644 --- a/app/shell-menus/bookmark.js +++ b/app/shell-menus/bookmark.js @@ -14,7 +14,6 @@ class BookmarkMenu extends LitElement { title: {type: String}, description: {type: String}, tags: {type: String}, - pinned: {type: Boolean}, isPublic: {type: Boolean} } } @@ -31,20 +30,18 @@ class BookmarkMenu extends LitElement { this.title = '' this.description = '' this.tags = '' - this.pinned = false this.isPublic = false } async init (params) { this.bookmarkIsNew = params.bookmarkIsNew - const b = this.bookmark = await bg.bookmarks.get(params.url) + const b = this.bookmark = await bg.bookmarks.getOwn(params.url) if (b && b.tags) b.tags = tagsToString(b.tags) if (b) { this.href = b.href this.title = b.title this.description = b.description this.tags = b.tags - this.pinned = b.pinned this.isPublic = b.isPublic } else { this.href = params.url @@ -62,8 +59,8 @@ class BookmarkMenu extends LitElement { return true } return !_isEqual( - _pick(this, ['href', 'title', 'description', 'tags', 'pinned', 'isPublic']), - _pick(this.bookmark, ['href', 'title', 'description', 'tags', 'pinned', 'isPublic']) + _pick(this, ['href', 'title', 'description', 'tags', 'isPublic']), + _pick(this.bookmark, ['href', 'title', 'description', 'tags', 'isPublic']) ) } @@ -106,12 +103,6 @@ class BookmarkMenu extends LitElement {
- -
@@ -143,9 +134,8 @@ class BookmarkMenu extends LitElement { b.title = this.title b.description = this.description b.tags = this.tags.split(' ').filter(Boolean) - b.isPublic = this.isPublic - b.pinned = this.pinned - await bg.bookmarks.add(b) + b.visibility = this.isPublic ? 'public' : 'private' + await bg.bookmarks.edit(b.href, b) bg.views.refreshState('active') bg.shellMenus.close() } @@ -170,10 +160,6 @@ class BookmarkMenu extends LitElement { this.tags = e.target.value } - onChangePinned (e) { - this.pinned = e.target.checked - } - onChangePublic (e, v) { this.isPublic = e.target.checked } From cbccc02fd4c8617a47973cc3a2a1e5ccb38432ad Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 29 Aug 2019 09:59:17 -0500 Subject: [PATCH 0020/1212] Rework bookmark shell-menu --- app/shell-menus/bookmark.js | 116 ++++++++++++------------------------ 1 file changed, 39 insertions(+), 77 deletions(-) diff --git a/app/shell-menus/bookmark.js b/app/shell-menus/bookmark.js index 052efe5e0a..390deeaf85 100644 --- a/app/shell-menus/bookmark.js +++ b/app/shell-menus/bookmark.js @@ -4,8 +4,6 @@ import * as bg from './bg-process-rpc' import commonCSS from './common.css' import inputsCSS from './inputs.css' import buttonsCSS from './buttons.css' -import _pick from 'lodash.pick' -import _isEqual from 'lodash.isequal' class BookmarkMenu extends LitElement { static get properties () { @@ -14,7 +12,8 @@ class BookmarkMenu extends LitElement { title: {type: String}, description: {type: String}, tags: {type: String}, - isPublic: {type: Boolean} + isPublic: {type: Boolean}, + hasChanges: {type: Boolean} } } @@ -31,6 +30,7 @@ class BookmarkMenu extends LitElement { this.description = '' this.tags = '' this.isPublic = false + this.hasChanges = false } async init (params) { @@ -54,16 +54,6 @@ class BookmarkMenu extends LitElement { input.setSelectionRange(0, input.value.length) } - get canSave () { - if (this.bookmarkIsNew) { - return true - } - return !_isEqual( - _pick(this, ['href', 'title', 'description', 'tags', 'isPublic']), - _pick(this.bookmark, ['href', 'title', 'description', 'tags', 'isPublic']) - ) - } - // rendering // = @@ -71,12 +61,25 @@ class BookmarkMenu extends LitElement { return html`
-
- - Edit this bookmark -
-
+
+ +
+ +
+ + + +
+
@@ -97,23 +100,6 @@ class BookmarkMenu extends LitElement { @keyup=${this.onChangeTags} >
-
- -
- -
- - - -
` @@ -124,19 +110,18 @@ class BookmarkMenu extends LitElement { async onSaveBookmark (e) { e.preventDefault() - if (!this.canSave) { - return - } // update bookmark - var b = this.bookmark - b.href = this.href - b.title = this.title - b.description = this.description - b.tags = this.tags.split(' ').filter(Boolean) - b.visibility = this.isPublic ? 'public' : 'private' - await bg.bookmarks.edit(b.href, b) - bg.views.refreshState('active') + if (this.hasChanges) { + var b = this.bookmark + b.href = this.href + b.title = this.title + b.description = this.description + b.tags = this.tags.split(' ').filter(Boolean) + b.visibility = this.isPublic ? 'public' : 'private' + await bg.bookmarks.edit(b.href, b) + bg.views.refreshState('active') + } bg.shellMenus.close() } @@ -150,18 +135,22 @@ class BookmarkMenu extends LitElement { onChangeTitle (e) { this.title = e.target.value + this.hasChanges = true } onChangeDescription (e) { this.description = e.target.value + this.hasChanges = true } onChangeTags (e) { this.tags = e.target.value + this.hasChanges = true } onChangePublic (e, v) { this.isPublic = e.target.checked + this.hasChanges = true } } BookmarkMenu.styles = [commonCSS, inputsCSS, buttonsCSS, css` @@ -170,37 +159,10 @@ BookmarkMenu.styles = [commonCSS, inputsCSS, buttonsCSS, css` padding: 15px; color: #333; background: #fff; - height: 400px; + height: 290px; overflow: hidden; } -h3 { - font-size: 0.625rem; - text-transform: uppercase; - letter-spacing: 0.2px; - color: rgba(0, 0, 0, 0.5); - margin-bottom: 10px; -} - -.header { - display: flex; - align-items: center; - font-size: 0.875rem; - font-weight: 500; - margin-bottom: 15px; - border: 0; -} - -.fa-star { - border: none; - font-size: 24px; - margin-right: 10px; - color: transparent; - text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3); - background-color: #bbb; - -webkit-background-clip: text; -} - form { font-size: 13px; margin: 0; @@ -219,7 +181,7 @@ form { } .other-options { - margin-top: 20px; + margin-top: -16px; } .other-options .input-group { @@ -235,11 +197,10 @@ form { } .other-options .toggle { - border-top: 1px solid #ddd; - background: #fafafa; margin: 0 -20px; padding: 12px 20px; line-height: 1; + background: #fff; } .other-options .toggle:hover { @@ -255,6 +216,7 @@ form { .input-group textarea { height: 50px; padding-top: 5px; + resize: none; } .input-group input { From 98451eae50b6f63e5e5cb9ce1328f75e0f47a446 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 30 Aug 2019 15:51:18 -0500 Subject: [PATCH 0021/1212] Add author and visibility to create/fork archive modals --- app/modals/buttons2.css.js | 17 ++ app/modals/create-archive.js | 365 +++++++++++++++++++++++------------ app/modals/fork-archive.js | 125 ++++++++++-- 3 files changed, 375 insertions(+), 132 deletions(-) diff --git a/app/modals/buttons2.css.js b/app/modals/buttons2.css.js index 8ac8c72f73..c8198dca18 100644 --- a/app/modals/buttons2.css.js +++ b/app/modals/buttons2.css.js @@ -53,6 +53,23 @@ button.noborder { border-color: transparent; } +button.transparent { + background: transparent; + border-color: transparent; + box-shadow: none; +} + +button.transparent:hover { + background: #f5f5f5; +} + +button.transparent.pressed { + background: rgba(0,0,0,.1); + box-shadow: inset 0 1px 2px rgba(0,0,0,.25); + color: inherit; +} + + .radio-group button { background: transparent; border: 0; diff --git a/app/modals/create-archive.js b/app/modals/create-archive.js index 49afad4b19..ff1b23e73b 100644 --- a/app/modals/create-archive.js +++ b/app/modals/create-archive.js @@ -11,16 +11,228 @@ const BASIC_TEMPLATES = [ {url: 'blank', title: 'Empty Site', thumb: html``} ] +const VISIBILITY_OPTIONS = [ + {icon: html``, label: 'Public', value: 'public', desc: 'Anybody can access the site'}, + {icon: html``, label: 'Private', value: 'private', desc: 'Only you can access the site'}, + {icon: html``, label: 'Secret', value: 'unlisted', desc: 'Only people who know the URL can access the site'}, +] + class CreateArchiveModal extends LitElement { static get properties () { return { title: {type: String}, description: {type: String}, currentTemplate: {type: String}, + visibility: {type: String}, errors: {type: Object} } } + static get styles () { + return [commonCSS, inputsCSS, buttonsCSS, css` + .wrapper { + padding: 0; + } + + h1.title { + padding: 14px 20px; + margin: 0; + border-color: #bbb; + } + + form { + padding: 0; + margin: 0; + } + + input { + font-size: 14px; + height: 34px; + padding: 0 10px; + border-color: #bbb; + } + + textarea { + font-size: 14px; + padding: 7px 10px; + border-color: #bbb; + } + + hr { + border: 0; + border-top: 1px solid #ddd; + margin: 20px 0; + } + + .form-actions { + display: flex; + justify-content: space-between; + } + + .form-actions button { + padding: 6px 12px; + font-size: 12px; + } + + label.fixed-width { + display: inline-block; + width: 60px; + } + + .layout { + display: flex; + user-select: none; + } + + .layout .templates { + width: 624px; + } + + .layout .inputs { + min-width: 200px; + flex: 1; + padding: 20px; + } + + input[type="radio"] { + display: inline; + width: auto; + margin: 0 4px; + height: auto; + } + + .templates { + height: 468px; + overflow-y: auto; + background: #fafafa; + border-right: 1px solid #bbb; + } + + .templates-heading { + margin: 20px 20px 0px; + padding-bottom: 5px; + border-bottom: 1px solid #ddd; + color: gray; + font-size: 11px; + } + + .templates-selector { + display: grid; + grid-gap: 20px; + padding: 10px 20px; + grid-template-columns: repeat(3, 1fr); + align-items: baseline; + } + + .template { + width: 160px; + padding: 10px; + border-radius: 4px; + } + + .template img, + .template .icon { + display: block; + margin: 0 auto; + width: 150px; + height: 120px; + margin-bottom: 10px; + object-fit: scale-down; + background: #fff; + border: 1px solid #ccc; + border-radius: 3px; + } + + .template .icon { + text-align: center; + font-size: 24px; + } + + .template .icon .fa-fw { + line-height: 80px; + } + + .template .title { + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .template:hover { + background: #eee; + } + + .template.selected { + background: rgb(63, 119, 232); + } + + .template.selected .title { + color: #fff; + font-weight: 500; + text-shadow: 0 1px 2px rgba(0,0,0,.35); + } + + .template.selected img { + border: 1px solid #fff; + box-shadow: 0 1px 2px rgba(0,0,0,.15); + } + + .visibility { + border: 1px solid #ddd; + margin-bottom: 45px; + margin-top: 5px; + } + + .visibility .option { + display: flex; + padding: 10px; + cursor: pointer; + border-bottom: 1px solid #eee; + color: rgba(0,0,0,.5); + } + + .visibility .option:last-child { + border: 0; + } + + .visibility .option:hover { + color: rgba(0,0,0,.65); + background: #fafafa; + } + + .visibility .option.selected { + color: rgba(0,0,0,.75); + outline: 1px solid gray; + } + + .visibility .option > span { + margin: 2px 10px 0 4px; + } + + .visibility .option-label { + font-weight: 500; + margin-bottom: 2px; + } + + .visibility .option-desc { + font-weight: 400; + } + + .visibility .option .fa-check-circle { + visibility: hidden; + margin: 2px 2px 0 auto; + color: #333; + font-size: 14px; + align-self: center; + } + + .visibility .option.selected .fa-check-circle { + visibility: visible; + } + `] + } + constructor () { super() this.cbs = null @@ -28,8 +240,10 @@ class CreateArchiveModal extends LitElement { this.description = '' this.type = null this.links = null - this.networked = true + this.author = null + this.visibility = 'public' this.templates = [] + this.users = [] this.currentTemplate = 'blank' this.errors = {} @@ -44,7 +258,8 @@ class CreateArchiveModal extends LitElement { this.description = params.description || '' this.type = params.type ? Array.isArray(params.type) ? params.type[0] : params.type : '' this.links = params.links - this.networked = ('networked' in params) ? params.networked : true + this.author = this.author || (await bg.users.getCurrent()).url + this.visibility = params.visibility || 'public' this.templates = BASIC_TEMPLATES.concat( await bg.archives.list({type: 'unwalled.garden/template', isSaved: true}) ) @@ -80,11 +295,28 @@ class CreateArchiveModal extends LitElement {
- + ${this.errors.title ? html`
${this.errors.title}
` : ''} - + + + +
+ ${VISIBILITY_OPTIONS.map(opt => html` +
this.onChangeVisibility(e, opt.value)} + > + ${opt.icon} +
+
${opt.label}
+
${opt.desc}
+
+ +
+ `)} +
@@ -118,6 +350,10 @@ class CreateArchiveModal extends LitElement { this.type = e.target.value.trim() } + onChangeVisibility (e, value) { + this.visibility = value + } + onClickCancel (e) { e.preventDefault() this.cbs.reject(new Error('Canceled')) @@ -139,7 +375,8 @@ class CreateArchiveModal extends LitElement { title: this.title, description: this.description, type: '', - networked: this.networked, + author: this.author, + visibility: this.visibility, links: this.links, prompt: false }) @@ -150,7 +387,8 @@ class CreateArchiveModal extends LitElement { title: this.title, description: this.description, type: [], - networked: this.networked, + author: this.author, + visibility: this.visibility, links: this.links, prompt: false }) @@ -161,120 +399,5 @@ class CreateArchiveModal extends LitElement { } } } -CreateArchiveModal.styles = [commonCSS, inputsCSS, buttonsCSS, css` -.wrapper { - padding: 0; -} - -h1.title { - padding: 14px 20px; - margin: 0; - border-color: #ddd; -} - -form { - padding: 0; - margin: 0; -} - -hr { - border: 0; - border-top: 1px solid #eee; - margin: 20px 0; -} - -.layout { - display: flex; - user-select: none; -} - -.layout .templates { - width: 624px; -} - -.layout .inputs { - min-width: 200px; - flex: 1; - padding: 20px; -} - -.templates { - height: 468px; - overflow-y: auto; - background: #fafafa; - border-right: 1px solid #ddd; -} - -.templates-heading { - margin: 20px 20px 0px; - padding-bottom: 5px; - border-bottom: 1px solid #ddd; - color: gray; - font-size: 11px; -} - -.templates-selector { - display: grid; - grid-gap: 20px; - padding: 10px 20px; - grid-template-columns: repeat(3, 1fr); - align-items: baseline; -} - -.template { - width: 160px; - padding: 10px; - border-radius: 4px; -} - -.template img, -.template .icon { - display: block; - margin: 0 auto; - width: 150px; - height: 120px; - margin-bottom: 10px; - object-fit: scale-down; - background: #fff; - border: 1px solid #ccc; - border-radius: 3px; -} - -.template .icon { - text-align: center; - font-size: 24px; -} - -.template .icon .fa-fw { - line-height: 80px; -} - -.template .title { - text-align: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.template:hover { - background: #eee; -} - -.template.selected { - background: rgb(63, 119, 232); -} - -.template.selected .title { - color: #fff; - font-weight: 500; - text-shadow: 0 1px 2px rgba(0,0,0,.35); -} - -.template.selected img { - border: 1px solid #fff; - box-shadow: 0 1px 2px rgba(0,0,0,.15); -} - -`] customElements.define('create-archive-modal', CreateArchiveModal) \ No newline at end of file diff --git a/app/modals/fork-archive.js b/app/modals/fork-archive.js index 36c456316c..9303c014b4 100644 --- a/app/modals/fork-archive.js +++ b/app/modals/fork-archive.js @@ -1,5 +1,6 @@ /* globals customElements */ -import { LitElement, html } from '../vendor/lit-element/lit-element' +import { LitElement, html, css } from '../vendor/lit-element/lit-element' +import { classMap } from '../vendor/lit-element/lit-html/directives/class-map' import prettyHash from 'pretty-hash' import * as bg from './bg-process-rpc' import commonCSS from './common.css' @@ -13,13 +14,90 @@ const STATES = { FORKING: 2 } +const VISIBILITY_OPTIONS = [ + {icon: html``, label: 'Public', value: 'public', desc: 'Anybody can access the site'}, + {icon: html``, label: 'Private', value: 'private', desc: 'Only you can access the site'}, + {icon: html``, label: 'Secret', value: 'unlisted', desc: 'Only people who know the URL can access the site'}, +] + class ForkArchiveModal extends LitElement { static get properties () { return { state: {type: Number}, title: {type: String}, - description: {type: String} + description: {type: String}, + visibility: {type: String} + } + } + + static get styles () { + return [commonCSS, inputsCSS, buttonsCSS, spinnerCSS, css` + input { + font-size: 14px; + height: 34px; + padding: 0 10px; + border-color: #bbb; + } + + textarea { + font-size: 14px; + padding: 7px 10px; + border-color: #bbb; + } + + .visibility { + border: 1px solid #ddd; + margin-bottom: 10px; + margin-top: 5px; + } + + .visibility .option { + display: flex; + padding: 10px; + cursor: pointer; + border-bottom: 1px solid #eee; + color: rgba(0,0,0,.5); + } + + .visibility .option:last-child { + border: 0; + } + + .visibility .option:hover { + color: rgba(0,0,0,.65); + background: #fafafa; + } + + .visibility .option.selected { + color: rgba(0,0,0,.75); + outline: 1px solid gray; + } + + .visibility .option > span { + margin: 2px 10px 0 4px; + } + + .visibility .option-label { + font-weight: 500; + margin-bottom: 2px; + } + + .visibility .option-desc { + font-weight: 400; } + + .visibility .option .fa-check-circle { + visibility: hidden; + margin: 2px 2px 0 auto; + color: #333; + font-size: 14px; + align-self: center; + } + + .visibility .option.selected .fa-check-circle { + visibility: visible; + } + `] } constructor () { @@ -36,7 +114,8 @@ class ForkArchiveModal extends LitElement { this.description = '' this.type = null this.links = null - this.networked = true + this.author = null + this.visibility = 'public' // export interface window.forkArchiveClickSubmit = () => this.shadowRoot.querySelector('button[type="submit"]').click() @@ -50,6 +129,8 @@ class ForkArchiveModal extends LitElement { this.title = params.title || '' this.description = params.description || '' this.type = params.type + this.visibility = params.visibility || 'public' + this.author = this.author || (await bg.users.getCurrent()).url this.links = params.links this.networked = ('networked' in params) ? params.networked : true await this.requestUpdate() @@ -78,29 +159,47 @@ class ForkArchiveModal extends LitElement { var actionBtn switch (this.state) { case STATES.READY: - progressEl = html`
Ready to copy.
` - actionBtn = html`` + progressEl = html`
Ready to fork.
` + actionBtn = html`` break case STATES.DOWNLOADING: progressEl = html`
Downloading remaining files...
` actionBtn = html`` break case STATES.FORKING: - progressEl = html`
Copying...
` + progressEl = html`
Forking...
` actionBtn = html`` break } return html` +
-

Make a copy of ${this.archiveInfo.title ? `"${this.archiveInfo.title}"` : prettyHash(this.archiveInfo.key)}

+

Make a fork of ${this.archiveInfo.title ? `"${this.archiveInfo.title}"` : prettyHash(this.archiveInfo.key)}

- + - + + + +
+ ${VISIBILITY_OPTIONS.map(opt => html` +
this.onChangeVisibility(e, opt.value)} + > + ${opt.icon} +
+
${opt.label}
+
${opt.desc}
+
+ +
+ `)} +
${progressEl} @@ -145,6 +244,10 @@ class ForkArchiveModal extends LitElement { this.description = e.target.value } + onChangeVisibility (e, value) { + this.visibility = value + } + onClickCancel (e) { e.preventDefault() this.cbs.reject(new Error('Canceled')) @@ -162,7 +265,8 @@ class ForkArchiveModal extends LitElement { title: this.title, description: this.description, type: this.type, - networked: this.networked, + author: this.author, + visibility: this.visibility, links: this.links, prompt: false }) @@ -172,6 +276,5 @@ class ForkArchiveModal extends LitElement { } } } -ForkArchiveModal.styles = [commonCSS, inputsCSS, buttonsCSS, spinnerCSS] customElements.define('fork-archive-modal', ForkArchiveModal) \ No newline at end of file From e474043888350991591a821f25f6cc7e99fd6410 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 30 Aug 2019 16:21:50 -0500 Subject: [PATCH 0022/1212] Replace json-renderer with simpler syntax formatter --- app/json-renderer.js | 56 +++++++++----------------------------------- app/package.json | 1 - 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/app/json-renderer.js b/app/json-renderer.js index e357d542be..d3fc31be1a 100644 --- a/app/json-renderer.js +++ b/app/json-renderer.js @@ -1,4 +1,3 @@ -import JSONFormatter from 'json-formatter-js' const TWOMB = 2097152 // in bytes @@ -12,51 +11,18 @@ function parse (str) { } } -var views = { - unformatted: document.querySelector('body > pre'), - formatted: undefined -} -var navBtns = { - unformatted: document.createElement('span'), - formatted: document.createElement('span') -} -navBtns.unformatted.textContent = 'Raw' -navBtns.formatted.textContent = 'Formatted' - -function setView (view) { - for (var k in views) { - views[k].classList.add('hidden') - navBtns[k].classList.remove('pressed') - } - views[view].classList.remove('hidden') - navBtns[view].classList.add('pressed') -} - -Object.keys(navBtns).forEach(view => { - navBtns[view].addEventListener('click', () => setView(view)) -}) +var el = document.querySelector('body > pre') // try to parse -var obj = parse(views.unformatted.textContent) +var obj = parse(el.textContent) if (obj) { - // render the formatted el - var formatter = new JSONFormatter(obj, 1, { - hoverPreviewEnabled: true, - hoverPreviewArrayCount: 100, - hoverPreviewFieldCount: 5, - animateOpen: false, - animateClose: false, - useToJSON: true - }) - views.formatted = formatter.render() - document.body.append(views.formatted) - - // render the nav - var nav = document.createElement('nav') - nav.append(navBtns.formatted) - nav.append(navBtns.unformatted) - document.body.prepend(nav) - - // set the current view - setView('formatted') + var json = JSON.stringify(obj, null, 2) + json = json.replace(/^(\s+)(".+":)/gmi, (v, ws, key) => `${ws}${key}`) + json = json.replace(/<\/span> (".+")/gmi, (v, str) => ` ${str}`) + json = json.replace(/^(\s+)(".+")(,?)$/gmi, (v, ws, str, comma) => `${ws}${str}${comma}`) + json = json.replace(/<\/span> ([0-9]+)/gmi, (v, num) => ` ${num}`) + json = json.replace(/^(\s+)([0-9]+)(,?)$/gmi, (v, ws, num, comma) => `${ws}${num}${comma}`) + json = json.replace(/<\/span> (true|false)/gmi, (v, bool) => ` ${bool}`) + json = json.replace(/^(\s+)(true|false)(,?)$/gmi, (v, ws, bool, comma) => `${ws}${bool}${comma}`) + el.innerHTML = json } diff --git a/app/package.json b/app/package.json index 192bb4ca4b..007f2b6630 100644 --- a/app/package.json +++ b/app/package.json @@ -48,7 +48,6 @@ "icojs": "^0.11.0", "identify-filetype": "^1.0.0", "into-stream": "^3.1.0", - "json-formatter-js": "^2.2.0", "keyboardevent-from-electron-accelerator": "^1.1.0", "keyboardevents-areequal": "^0.2.2", "listen-random-port": "^1.0.0", From 7cc376d8abf3f4cda59bfa42adb8c355de9f8bee Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 12 Sep 2019 09:16:50 -0500 Subject: [PATCH 0023/1212] Update uwg api usage (no filter: param) --- app/background-process/ui/view-manager.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index af0ce65e92..baa3b2cfef 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -610,11 +610,11 @@ class View { this.numComments = 0 var userSession = getUserSessionFor(this.browserWindow.webContents) - var followedUsers = (await beakerCore.uwg.follows.list({filters: {authors: userSession.url}})).map(({topic}) => topic.url) + var followedUsers = (await beakerCore.uwg.follows.list({authors: userSession.url})).map(({topic}) => topic.url) var authors = [userSession.url].concat(followedUsers) // TODO replace with native 'count' method - var cs = await beakerCore.uwg.comments.thread(this.url, {filters: {authors}}) + var cs = await beakerCore.uwg.comments.thread(this.url, {authors}) function countComments (comments) { return comments.reduce((acc, comment) => acc + 1 + (comment.replies ? countComments(comment.replies) : 0), 0) } @@ -647,9 +647,9 @@ class View { } if (this.datInfo) { let userSession = getUserSessionFor(this.browserWindow.webContents) - let userFollows = await beakerCore.uwg.follows.list({filters: {authors: userSession.url}}) + let userFollows = await beakerCore.uwg.follows.list({authors: userSession.url}) let followAuthors = [userSession.url].concat(userFollows.map(f => f.topic.url)) - let siteFollowers = await beakerCore.uwg.follows.list({filters: {topics: this.datInfo.url, authors: followAuthors}}) + let siteFollowers = await beakerCore.uwg.follows.list({topics: this.datInfo.url, authors: followAuthors}) this.numFollowers = siteFollowers.length } if (!noEmit) this.emitUpdateState() From ab29d2fead58135732993fb339724468542b1cb9 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 16 Sep 2019 15:39:45 -0500 Subject: [PATCH 0024/1212] Rework user modal to handle save/update internally instead of passing out results --- app/builtin-pages/views/settings.js | 1 - app/modals/user.js | 9 +++++++-- app/shell-menus/users.js | 6 ++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/builtin-pages/views/settings.js b/app/builtin-pages/views/settings.js index 139cd57f66..fab83effa9 100644 --- a/app/builtin-pages/views/settings.js +++ b/app/builtin-pages/views/settings.js @@ -149,7 +149,6 @@ function renderUsers () { e.stopPropagation() var opts = await beaker.browser.showModal('user', user) - await beaker.users.edit(user.url, opts) Object.assign(user, opts) renderToPage() } diff --git a/app/modals/user.js b/app/modals/user.js index fb523dffec..a69c945fac 100644 --- a/app/modals/user.js +++ b/app/modals/user.js @@ -172,14 +172,19 @@ class UserModal extends LitElement { try { var thumbBase64 = this.thumbDataURL ? this.thumbDataURL.split(',').pop() : undefined - this.cbs.resolve({ + var data = { title: this.title, description: this.description, label: this.label, thumbBase64, thumbExt: this.thumbExt, setDefault: this.setDefault - }) + } + if (this.userUrl) { + this.cbs.resolve(await bg.users.edit(this.userUrl, data)) + } else { + this.cbs.resolve(await bg.users.create(data)) + } } catch (e) { this.cbs.reject(e.message || e.toString()) } diff --git a/app/shell-menus/users.js b/app/shell-menus/users.js index c9467072ef..cf6561e0a1 100644 --- a/app/shell-menus/users.js +++ b/app/shell-menus/users.js @@ -141,8 +141,7 @@ class UsersMenu extends LitElement { async onEditMyProfile (e) { var userUrl = this.user.url bg.shellMenus.close() - var opts = await bg.shellMenus.createModal('user', this.user) - await bg.users.edit(userUrl, opts) + await bg.shellMenus.createModal('user', this.user) } onOpenUser (e, user) { @@ -152,8 +151,7 @@ class UsersMenu extends LitElement { async onCreateNewUser () { bg.shellMenus.close() - var opts = await bg.shellMenus.createModal('user', {}) - var user = await bg.users.create(opts) + var user = await bg.shellMenus.createModal('user', {}) bg.shellMenus.createWindow({userSession: {url: user.url}}) } From c86c65774f1a165184355046c726aaf609a708b7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 18 Sep 2019 09:55:59 -0500 Subject: [PATCH 0025/1212] Update favicons and template images --- app/assets/img/favicons/addressbook.png | Bin 1022 -> 0 bytes app/assets/img/favicons/archives.png | Bin 0 -> 317 bytes app/assets/img/favicons/bookmarks.png | Bin 1472 -> 1203 bytes app/assets/img/favicons/library.png | Bin 991 -> 0 bytes app/assets/img/favicons/people.png | Bin 0 -> 1345 bytes .../img/favicons/{start.png => pins.png} | Bin app/assets/img/favicons/search.png | Bin 1157 -> 0 bytes app/assets/img/favicons/services.png | Bin 1592 -> 0 bytes app/assets/img/favicons/status-updates.png | Bin 0 -> 363 bytes app/assets/img/favicons/timeline.png | Bin 293 -> 0 bytes app/assets/img/favicons/watchlist.png | Bin 1569 -> 0 bytes app/assets/img/favicons/websites.png | Bin 716 -> 576 bytes app/assets/img/templates/website.png | Bin 4185 -> 5585 bytes 13 files changed, 0 insertions(+), 0 deletions(-) delete mode 100755 app/assets/img/favicons/addressbook.png create mode 100644 app/assets/img/favicons/archives.png delete mode 100644 app/assets/img/favicons/library.png create mode 100644 app/assets/img/favicons/people.png rename app/assets/img/favicons/{start.png => pins.png} (100%) delete mode 100644 app/assets/img/favicons/search.png delete mode 100644 app/assets/img/favicons/services.png create mode 100644 app/assets/img/favicons/status-updates.png delete mode 100644 app/assets/img/favicons/timeline.png delete mode 100644 app/assets/img/favicons/watchlist.png mode change 100755 => 100644 app/assets/img/favicons/websites.png diff --git a/app/assets/img/favicons/addressbook.png b/app/assets/img/favicons/addressbook.png deleted file mode 100755 index 42b0a8903a2ff8f4e4f43ebd5e06442ee1b97a6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1022 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz~~j=6XFWwns~;IBFaNRcGtNq zU})x+1o;IsFfuW-u(GjpaB^|;@bL=?i-}7}N=YlLs%dKL8Wn=IW~Moc7x^wK%i2 zrQ?osXGg#~&#jsN|A$A*m)*`xd;Wy?d2aUk_qXTHH7;&@JjG$(=}BkgmUden$z1%^ zug&e&s*~;?Y9jYtvuV~a7rv}DAxQ7^M1^XXdsBOsaD^Pt?{0Q+IjON?`c-zWriKL5 zr}96f8*WP1>!c>j&12B`_-p|~(#Oy1_BgOSQB`X2Gbt^0U}183uwvb(vI7kZR^Lrj zXehFJAjiRzc)FdL&BZ{0;Ww8d&y8#jk-01nWSAFim??3z!GI^?0E0Qtiz5u$8)iJ( z_2)3#X9ocmaV5rYzt(=b>NLZ{_(+t*ii}UE`3iIu*4h7``~S{SMS%lt8Mm#Kj}UJUiq-I?tS-#@J*9 zUsjcx`f2_nNvhu*%k1mr|I9!w1v5+bK*a0ii57NxvxYnM{Pt;Je zXDu*Z%YM(?sVa{ro?lIRt;dSXH@n+6JMUkjXk6t&e{&ocS%}f9KZrVqnr^@O1TaS?83{ F1OOHJi|_yd diff --git a/app/assets/img/favicons/archives.png b/app/assets/img/favicons/archives.png new file mode 100644 index 0000000000000000000000000000000000000000..14d9f1cb064ba028d999d322215bb2f068a08b58 GIT binary patch literal 317 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sfdM`tu0YzvGmeTVUC?;}&`_z8 zAirP+CN>TpF&RZ=6+L}J|B{OOrq;IA?>~S2{`1__Z9YI%jh-%!As)xyUOCIvpdjFO zai!*IqtpN1{*^Z|SoAhsVCRh8^O&tVT<%}jWxsPt@Tq%44gkCK#^nwy=Tprfd&ud=nbxw^c-!^gFw|F z^Y!=n`~5sDP`dyC0xwBKK~xwSt-*^sQ*i*t;qPx_HkTxnONykV3l-f}L{x-QxmI*h zB&kp_B)8e;kM5jpJhpQ%J-x5~leC_b+Ls|hKJ7z3(4u{K4TMsav|Mb>O4>_o>#0i8 zAz&3~e^QbLfYMo5OHon|0`GMeXjD@Afg+hXSoJCi*}#aTbHH6Cp%*BYWP-5b)t<6| z53*eV>b0lmK#6Q-9+o}Yg$b0Le*k<`R1GkEwLIG+2Y+GJy}@Z>jw{M65cT_ATWwjE zM-l0_`zFI|H>OROh%^7D^@KD_yqUox+GM&uf2|WQPwm76hg++Ur5h#i9jUW= zwj2ym%QuX+lzt|vLJnEj)*HQwb7~6P9XXT(;fJYms0so-awvc&0l6`?OSH);&##@B cLXZC0Um5NbY4s(2cmMzZ07*qoM6N<$f_5iX%>V!Z delta 1024 zcmV+b1poW93BU_1iBL{Q4GJ0x0000DNk~Le0000$0000$2m=5B0G+pi>;M1(*HBDU zMF0Q*u|bakkx)f{lqg@}0001-bW%=J009C61O)~M2?`4g4Gs?w5fT#=6&Dy98yp=T zA0QzjBPAv$C@L&1E-x@KGBY$bH#j;wJUu=?KtV!7L`6qQN=r;lPESx$R9075SzBCP zUSDBiV`OG&YiwfP#aDh>44ikCBp;m6w`-ot~qmrmC;7w6(XoyuQH0 z$I8sk&(YP{+T7yg=IH6`?eFpP_4oPv{lOadz5oCM{7FPXR2Uh}!3kFrXBY<1XNC}g zC@K*V6%;gDqAg%?w*{+4aK$RPw2BL+RIU47af{Ri6~sVH=J~O{-y~y3m~Tkp={equ z|JgoXz_$p0lZ@gU+$Q-7r3+JOLCJ1X`46S4Vb)rd=sL4rpfn5Uh@d1_DaI)hD8+mV zYyKF;YLws##Tooc@f^jgrcwd(14T26c8OvDKo`YR6l)%pN&w~r#Yz-uh~hkyT@;T| zoDh{NDCS3sMieDZaS>TDieVI^ib@DsW|Cq#3KtxIqWBArZi;{BV0Pumx>iZp~6Wjf5NeowkQg-c#6si)-*v!~} zBpsRl0BlbvOlSot?m`$_$LTiJZ9)uC)Ih72q94Fdqs&P#=fidb*g>%#TJ03ASUtj= zhjo~DjX7^yv7F)vw2o68dd!?>E1)d8$DENE#Vu&vr_0MVa4a3B%NVpK`Ni8dxHTu3 z7l1~Pj`3{0000S5Q<^R#DT|)zddHGBz@JXlm~6pEY~l;-$-0tX{i*%l2J+_8mNY^!SNW=PqBp ze)HCydk-ExdG_|>=da&?{{H(qt?VE$N-ul5IEF+Vemm`E_+rH3?qvOr(VgcNz!Zb9xO>_7E|38tP@21DOoy_Zh-YfF2yFb(C*}0x&tt~fRw6(6A zC-=9n`);|{$2MoVz#7H#>Ukd)>inNjTXWL;^Iit_nLBJdKbpHU#5x_cDv&9>z9{{` zqJRQMp2}|F11kdp*n{pRsV8u;GI-`2@=o}6>6*x7t{|2T(Hd*^+aPrOqyPLFQwIU&btk4a?FwYU21=?WP`SU%T3h>D26-Cz2cYwJ^2! z#vMGM>osLjo+DrAve>3XhJX6swl8PhqsOp+Zqqfb1Lhw)CopR9H5PJyi2LU~y+OD= zK|CRB-g%~)Ev%oto=;r1b%uj!BBz_Ezsp{p37fAuZep9E`-$-hW7wLh8Gjg0oce6c z;99Nup7G4d&&>?xg{xrmV+01$6_S(Fd;5i3wSRPn%_Z-93>7N*L9$WmAXL0`Y zC%r*laQlO98+(Qi*DEU4Fm70E(P;YHE0Af$iTqOD)CR-d8<;jIp8FoltYe{dmtpF} zZA%)$gO7hVkDR`F!Jgi~MfXd-OiHS=eUX9- Wd0%6jO<92ni^0>?&t;ucLK6UFk$=4a diff --git a/app/assets/img/favicons/people.png b/app/assets/img/favicons/people.png new file mode 100644 index 0000000000000000000000000000000000000000..51cebce905e88d1cb2724b4105db6f4a2eb14f78 GIT binary patch literal 1345 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBgK_U_2V&6XFWwns~;Iq9Gju=F*Em z@j9&}$S;_Ik%^gwm5rT)lZ%^|k6%DgNJLazLQ+abR!&|)QAt@vRZU$(Q%hS%*TB%& z#LV2n($>!2!O_Xt)y>`0%hx|3Feo%UGCC$UEV|GJ#X>SWh+*#Ub}w7rY&2y@7%R#-+_aNj~+j9?&|fMx9&Z7^yJyg*Kgl{ z{QUL%&)Eak7A=!J7-B~!D;rPS<)jKni1hyUt+_G$uTeNDIn2SvA z(VJP(;jiyK$`IUoCFs=wUa!6fKWgKO?^H)lXPNK*b%x97Mbla?rhDeueV=pSb8+nF zD|1fr2IpF&UzzYyRy}La^%eWRE&uUJe4CE6S}-bcBK`lK=3 z7j!-HF0iC_UzV=Kf{YV^TYm^A*etFJ^JVCd2`&1^{;y~Cz8?w(k5(MgeXwWZ(-i-E zbrru;u1!qUV>A!_W?SI8(5}B>`{5H(3}xCT|CRq_i*)b?nLW`;_}AR8yXoD5kRZnA zhEtEpus#z_2x#QFpqt&0WuEetN!#?Xe1*>I?5xrgQq74!Z#Ar06PRT?=_~Jt)~>!o z#*v(hgf_(;%GdhzfITvD(Y4dB+V?DRb?^!^54@eSm4SPyoJ4qrc;If)DrVbDoEEDC z)B`PB&mAs&Y4;{j!}m&3>#v9FzA%}sO6iJXT^yipGub9$@uc}HOI=j66N9SwSBD6l z4nA8PdWrG<63!PPk=9qkR_L&=UuyUwxV1Or)XGDC6-u$Khq-pdg~F_ zU#96xnLju*evzGV>O@KZ1L>EBzfLtT+`^>1AwG)N;!|$=v5%IMZtvQ0b?T$zGd9NB zvA>w`$YFQawZy&lo9$kn@J#XFdpK(OqNfeonG$*)T~9M}E=?@qe7AZ9_YCE)mwon| zh~HSU=!S{Rl(@@hv|kE{$o#Bp;$LZUc4@Hug9T|vxIbK%oAtX;fUA(TjN!70Iqw%u zlT*(nLOru)-4qSdI&HMubk&u^IonpPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1R6<1K~!i%?O01l z6;T+rO+~MDEsJW`rcG^=BnrgBXQ`+&qmred1-HClWOy^FcdB<~a8ux>bJ9p-s|Np-8oHI!YB#?kVjKtbPsGSC@9D&D=VAB)&4#5^YfQNSTh&^#W)#&sU~mKL`%GKOjb!)ZRJ(+eK+zlTlfR$)fd>Z6f*qjZaEqBk=$b2_Kh2LwOn zh&PKz9WuLb2Sr1FSX_BNEIMD0kggm0Lt@XJpl2QP+#?xNF>#R7>H#g42!>m0oKPk<8mx}fPYkrfBG`}}n7Ug{T-F^FU2jHO zxA}2M{#T!?q-RvdHc`?U`J9$oI}bQ$7{*+d;U5S@uLm+Ht@CO^B8j;0BqUBf2now2 z$df2TWm_?n+y;p1K7oeIW*Pp0AWe2a+KAi$;A5-1{b)y|{ zN)90e`8|ox#di`T_g$9Z9|+WF2Q-efdM4FLbr}WgS%!Zg$e0_MZW`q~R0CNo!#`*{ zM^}#eDxs4!L>2Lad3kxWS;oWw1I}(tCn;YCn7@%;<|_9&29R^&yRZ!eqcd>W4sM?8 z@)j`xnvt*5v~(}x^m+gTN1X&XeCQlX@<3yljsfJV=Ct}yUS2)}fqNYjI0i;_kROF4 z-cXK|;H#J%DGBuH111KXWb#mt)0wY1F(ikg+VaewU>>K`4;WY$VFIMd7tNchTx}}% zs-oSDtqD4>!o*!yQn`lHW=Ff}pC7g!%1z3(&PyWJ3(#34JpS5W}r_>Lg6NkX> zYT_svLj7urh3gF41jLKgX_;c>nl!O4J?A*5j16dBAnEC7jBwdT%d!@77zy0VD1ZqP zzXIZkaWO%lPv#Jo4H5PDrPb;qNb5QABj2kBv&QCKGWn6RA*(K7 zVlp6MVzS`BHIbZ}{+=IC4X7qorKW#KNzruRi6>IhGR`EYW(-V8@d~E#ZvqMEMN-l) XMc{7JfBfZ_00000NkvXXu0mjf+{Xj) diff --git a/app/assets/img/favicons/services.png b/app/assets/img/favicons/services.png deleted file mode 100644 index f7341f8c3e35cec92963b883aa51fe7cb934ff4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1592 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz<4XbC&U%VHSvraMZ+%yF8=jz z21RyBkY6wZBNHnub+QFU{FYSL}W~CTzq0uN_s|Sc1~`7L1A%8X?aCuO?^XC zYiCbi|HMgCrcR$RYxdlE3l=S2x_a&U4O_SG+_h)lfrE#SpEz~q-1&=_uU@}->&~Mm z&tANI{r3IG&tJd){QXyW+y6cW2Ic}!7sn6@$*Tu{J5NrPV0-Xewk}82Wsc*L4Q(E6 zQ`b${B%G)@QB-QuVXwm`TZ~>XO!HK9Iuo$i(EZN;?DL=Zc-&5Td^^81_Q(CMEk`G; z{&zEN%Hxe9*W-_doVTcj}YQFYXAObW>|acSs?F`^vZmOGm@r zwV}bW08I1U3z(c((VVWuS3`q>=xW=j9Qg9gLBnhgS`CFaY18{15~%4S%LR6meS?@RvhE$~j$%Ow#9W4qE5)&+h!{5rAk z?}vNKXS7(lZ0v~aI&9$CRas&9tmDv*-6DStnnl75{XTN~%vmLVPs6cb%HkinBv`V{Y_W|C=e9QV?vH46{CB1v4RN}pbyM!CJSLfuFmg?PX;}u^yugTI$ zwE1*UOw-Me>Y=*Z%)CGH`nb+|l+N&cg;b)IMq0p;-74={4b)fgF=Z^=CD{1VJAJaK z#-XpZ-|j6oxEu144edoF$T4EV)Wy!6}O=t7pHm`oMkFZc297D~%6kS5;1l zEY4(&%3P#zesxGB`wr7p${OdHuBLWqhj43n8=qC|VDS|`RXA&z)S_8wEU5*nLrxo< z4q46ju=L`bM$b!cTr2KhI+AFWxlU<|#$E=s1>&v|>kGW~mDfbyxn$#%l_JQ?@myJ~ zkZa+N&??Rxj|<+`Ef+oY8+|#e3R^y!`Z8GxnZzr$c+Fz@q>`bvaj)I90*TwR(>cvI zu4S7R(*Jf&is#`QZQhr8793zW8_&1&Xy&=ciqA9Wxn+j0NRyF!v@S5#&C{{WcL`ID jqQ~0KoQD51>KX1=-V!eOB>Eg!NHBQ1`njxgN@xNA?YDX! diff --git a/app/assets/img/favicons/status-updates.png b/app/assets/img/favicons/status-updates.png new file mode 100644 index 0000000000000000000000000000000000000000..30431c10465b3155424630914ef398c5c21ffaf1 GIT binary patch literal 363 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-su>n3Iu0YzvGmb{+hidZ>R3& zJLJH_((c}%+}6UMy-)ePVhU-Nk<`lCx#_va z)}5EPUurz2ZsMxpB-r)gf75QQ+?cI`4Noig^}No!>)vqnY5HB^UnYXruK#Sm)c&zW z&2>fIB){%CQ+>K}-rclOJNj06{_!PWNgIKQ5oPp!lvI6;>1s;*b3=DjSL74G){)!Z!;4V)W$B+!?x0g3^9(E9Eei(h!JN+Wh0~33- z6%(Zzxoa0ZWAVQ_cjnCUU3T|>+uPW0$#1_o$9h4n^_gdP7~kGbG<(jMai4Fq@#LcU z9>2?GoZZQMYy07xbFvrq$)***Yxwroz}i^zdnc8(8Luxo$FJ|O?Dmn@jx9i6FnGH9 KxvXPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1-3~gWiuwz zO-Ct0r7di{EDR{F3>Yo6&_c_-mqK|hlokqYX-nJVbM8Hb_TEcr8MtWfFL`pC=A7sG zpXWU1x#vIL} z+IN)I)%(b_8_Q{pW)FxhR8;6pU?^8BRq5jJY%Q&*dHaM$vx~U<_aMT*gpAG$#pP?T zm@&L_Ck%#Li2I!dXAJ^R3WZ#+K{5uFYTXMY%=V*HHSUF4u96DQn;xChU0P}|_PA`! z=*XOZVSXhNjRZq0Vb8qZ+dIUJ)EYX+e$_UHrRU?_&?XfX+LCQAs}i;DX?!T9f}X0Z zOl>qAklJp}b069&P-v52Kmc$w%z zNw5pk^XNU36FqXN2kq7y(e1Sz=&@!KD(Yj<74ITHkl^(%desdrb3)(PAYlQB{SuL} zH9+`R{I+H~|4r8voSE}Y5*-|tg=O?qYagO&{?2Nz7;VTMijY}6w0qL^i>6+bBq-S^ zZo5QI?tsq%5ML&82&Qc@j<3e!aXvIp!;BB@flNyYma@Apb|RC1iOl8+uSCP@`c~0y za0yiDjuL6Pfe(_n1)$yxuh^7Ijv~3xEG`rhRloauLS{ zL>s$?)yXo#RtpK9g4TT>kzdZ$LRRR-T{0Vg@UM0i^bwo_Vr28_~#53 z;!PT(#YU&=;!SJNXf!ty`CQ$X3kMlY{h|Y#P81G+Pzj`* zG5XdHw@@E$@kIVQ7AO0>#lgh;98A)?u$a|z3>Huz5VKPYaL>%PgEj=(7OhU_Lqx7L z76+uJ#7(pcKBqk|~vWcvfloY>kD9QZqTNa;!rs-Sz9BdWm(7_~JlhcMoGMRCiYx3lGnY$P6LBh*iJe~-_ zd&kIUor`r6{?4y#HUb>o1Uv8@UI2DJz_={J!)*%$(X(w9Nx|v%YKKjf`CvHgunvzf zoOlPbVi{%%N$BkbUhOYJO-^$ef^8mw-GG~Qkm%q(XzJa+r| zofaqkE_AFjkkxBYS~KuF0?!aUC*i* diff --git a/app/assets/img/favicons/websites.png b/app/assets/img/favicons/websites.png old mode 100755 new mode 100644 index 53d8dcc13f87ff8e7b2271245535f087bf1d06c5..91c9ee7a0f08559a751520642aa78988eecf410a GIT binary patch literal 576 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-s8v}eoT!FNSXWS4%v0Be&108Ku z666=mz{teH%EisYFDN7|A}S^>A*Z0IqM@Z@U}9?N85$N5pOli_**$mZvh`cG?>cwu z(UX_&KYsrD{pat$pBW`5FfcH(c)B=-cpSfaEjzf$LFCxSbn&1-lU^g;-SKbIycQh$ z{MSB0f7YB&Io#=OGe7;Ws%Mso00!Tp8?l!UgjOHCU$vW4_Ce>oOZ!+_`3p2zPcEDq zJ4ye#%IAH|Dt*Sb>hBA^jP8ds%$abenyX==NL|C@rZc;^8WuO5Nn$qtoAxI!W&_2`Mg`mcPB8I0P$7Lp^?;2Q;{{JMUhT&Go3k1?U5|06^~L7a9N6mW zEMGD8_4RA)Il&@VtW`~KNOwfVO|V*)k}dnXImX_&9w;^=YrUhb>et!~l|>xZX4n3+ z=uMhbw@0fuO#2tp_N&+Q&GwrejBQO8IX^%CD8B*2y7lr)>mCC`mBG{1&t;ucLK6TP C>CRmM literal 716 zcmV;-0yF)IP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0%1u+K~!i%?U~PO z(_k3K`;R#P5b+>1AOq*{S51GRP0)#*dQj2J4hjPm##W?*UZzEvh*F(X)`V4A-9Zln z2MRjU!ww>dig+;z_?RcYZx%}!ZxTnse7^9xq825>IY$ON%`xwdUF=$v`%CmwHHCXFy{GTCKgXzPWvu5C^;o= zbO&9oFAYNOeA&HTtd`;D$K3#*ceym1`@N+@l`2@46`QAvFFa4o#Nvs|Uj$Er7eP)= zz}y6vo#x@e29;3 zY=oK@^u*nK1~ajE;?jC{mmAEyv0(DG8Jh)Pk;Y2!J^kQrK7*N9JaJKxO`sxdhKg_l zD$)lPVKY>O8&Hux5-)KTC;Dhz0000c5J<$) z!Pf0y794;A9hZ7G>Wg5F(GV z*K=}Enk{$TIJNT_cPd5|{mO*nZA&=o|LRL`;YZD?$jOC;1>K!+j{xGz^YMrKwP{Ea zO+f}{Q|$*{6TwK!;nrZ)(P^y%@ET966#jQ~2C;*0UOoU#GX&-(iC;h$%aNppwC%$; zgha}dAj2lle8Qn@?Xphr?EmMO7ZY8GwC-;G&d<-^HujktE}+aDs?RNX{DI0n##Y+h z-K8cc?`&>n#o^MCPC-99{AK`*$*#tgB1YZD?pX2^SR=&CSViArR1L zG{MEi?(!!R9D=qqHEn5awT`cQp&x{OrnD&7Ml)Ffyg_d$(@?*VC^V@&uq5j zW(t#@pD6*+jjXs~)V0Vs zfvTB$jCf}HVz^Uo(YwaLxihTYgTSJ$CDZD)34Qtb5+0H#Nem(e5nu|M5;PMuHa3Pr z>|(1Bp7;uqWk*3c|Yf&OI*vbcXel)3~MO<)#XXT6?Ndmtx23 z>iz9)ITD|RL@515_z7H9rxOD8vt#quFWw?u@5&!Cx32#1;PiiSXxoF*uCX_iXtV^LV^A-&8(Um#cBpn?j!qB4(%PqQhgwK$D3*q0n?-(KFF(ozA7Z zxzzuySpH!`)pnsP(j7EEI-W`crg5$Lds!~}7US$u6Kh~y1rfEfvSK+mTQIBCn^^v> zN~|=woMsU(Q&D&3J!&u&P+sM5IDLJ6T&@nn=?N4yIS9EY$)?b;;F`2(1e-zkJDT2Q87^0Se$&Dfd? znL;)XSgMiq2m1JGvZv;EC~7@C2vO_fhB1>)mwuJD5=Wbt;0}2R*pXcW4Y#v?L4#=Rkz-RQ80%B6VauzvNdO*? z&*rt>E02b-lsKP1SGr5M8M{7>w9{Zr8U<8xmxEyJ&2&KUo@FUGFzW?w87P6pu7r0; zo7=FeUmiJ{Y&Wfy^R_J1upXKcb^SWGxy(PpWkN-2GzlrW=B0eDRQ;Ih#Ws~1&M47a zjH&U2OzZz3dkNC$S@i5ka%zWDs$sP!e%;%5e|f<^i)UFHSR8GX9)t1UV!ROQ_u^7& zt`Fr9$9z_r^{i%l)mYUdJm-KFToZle?IVjIsEtNg=_~kpnmVTed#JvY4Ie3JoC7f$ zBlwyZOn_$9DDyOZU`RcC+yGTf{Y}Wj*LkJY{iP z^xfQTd$DV;sR=!qA9AbBFJ!w#mnwOw%!pO*`=VhyE|(o~KEO+B`tp?$jboQ4M)#ZF zK2z##3dv#Z=V_E|(%GqRZ4)eEVWIZ#!zd}(KIe9WjL4*n%UFGsjcKLFKZ}b7SR6Ve!#JJFmwo$Hz}5NL+0YC*ZO=SiTrmn!RQ6In zd}CuH>i!D%f~#EYk=cr=o5zl0C7o}ZodaGY5v>IoaQw^d{>r%WC3EQ*UiUJY5GVZ2 zPL6Za0*8MnmZSfNda;I;h5)npvL;QfeQ_kJP!2E}|1>>aB&y|IM_H|u?fP-GQZyXu zuC35<+(yQCXc#OK7};}oGz@yzso1;>>0D1)-4+gvcXSsp1nx^-@w<)8Mn}x2F)+>3 zg9`@0psLH!0eGc{Wl{3&{8n1FxUn delta 555 zcmcbpeN#cPGr-TCmrII^fq{Y7)59eQNT&gD5C@U`2YC+}h_D2 Date: Wed, 18 Sep 2019 10:00:47 -0500 Subject: [PATCH 0026/1212] Update asset: protocol behaviors --- app/background-process/protocols/asset.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/background-process/protocols/asset.js b/app/background-process/protocols/asset.js index 37002b7d9d..d672e2acbd 100644 --- a/app/background-process/protocols/asset.js +++ b/app/background-process/protocols/asset.js @@ -59,6 +59,11 @@ export function setup () { // if beaker://, pull from hard-coded assets if (url.startsWith('beaker://')) { let name = /beaker:\/\/([^\/]+)/.exec(url)[1] + if (url.startsWith('beaker://library')) { + let match = /\?view=([\w-]+)/.exec(url) + if (match) name = match[1] + else name = 'pins' + } return fs.readFile(path.join(__dirname, `./assets/img/favicons/${name}.png`), (err, buf) => { if (buf) cb({mimeType: 'image/png', data: buf}) else cb(DEFAULTS[asset]) @@ -73,7 +78,7 @@ export function setup () { data = await sitedata.get(url, 'favicon') if (!data) { // try fallback to screenshot - data = await sitedata.get(url, 'screenshot') + data = await sitedata.get(url, 'screenshot', {dontExtractOrigin: true, normalizeUrl: true}) } } if (data) { From fcd7b29652be18287cae326295c125eb8c9b820c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 18 Sep 2019 10:10:06 -0500 Subject: [PATCH 0027/1212] Readd site-info dropdown UI --- app/background-process/browser.js | 19 +++ app/background-process/rpc-manifests/views.js | 1 + .../ui/subwindows/site-info.js | 138 ++++++++++++++++++ app/background-process/ui/view-manager.js | 5 + app/background-process/ui/windows.js | 4 +- app/new-shell-window/navbar/site-info.js | 44 +++--- app/prompts/create-page.js | 3 +- 7 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 app/background-process/ui/subwindows/site-info.js diff --git a/app/background-process/browser.js b/app/background-process/browser.js index 42f0587b47..ffbb5439b9 100644 --- a/app/background-process/browser.js +++ b/app/background-process/browser.js @@ -18,6 +18,7 @@ import {open as openUrl} from './open-url' import {getUserSessionFor, setUserSessionFor} from './ui/windows' import * as viewManager from './ui/view-manager' import * as modals from './ui/subwindows/modals' +import * as siteInfo from './ui/subwindows/site-info' import { findWebContentsParentWindow } from '../lib/electron' import {INVALID_SAVE_FOLDER_CHAR_REGEX} from '@beaker/core/lib/const' @@ -154,6 +155,7 @@ export const WEBAPI = { openSidebar, toggleSidebar, + toggleSiteInfo, toggleLiveReloading, setWindowDimensions, setWindowDragModeEnabled, @@ -165,6 +167,7 @@ export const WEBAPI = { return modals.create(this.sender, name, opts) }, gotoUrl, + refreshPage, openUrl: (url, opts) => { openUrl(url, opts) }, // dont return anything openFolder, doWebcontentsCmd, @@ -270,6 +273,17 @@ async function toggleSidebar (panel) { viewManager.getActive(win).toggleSidebar(panel) } +async function toggleSiteInfo (override) { + var win = findWebContentsParentWindow(this.sender) + if (override === true) { + siteInfo.show(win) + } else if (override === false) { + siteInfo.hide(win) + } else { + siteInfo.toggle(win) + } +} + export async function toggleLiveReloading (enabled) { var win = findWebContentsParentWindow(this.sender) viewManager.getActive(win).toggleLiveReloading(enabled) @@ -596,6 +610,11 @@ async function gotoUrl (url) { viewManager.getActive(win).loadURL(url) } +async function refreshPage () { + var win = findWebContentsParentWindow(this.sender) + viewManager.getActive(win).webContents.reload() +} + function openFolder (folderPath) { shell.openExternal('file://' + folderPath) } diff --git a/app/background-process/rpc-manifests/views.js b/app/background-process/rpc-manifests/views.js index 70f88e6daf..b873b632e8 100644 --- a/app/background-process/rpc-manifests/views.js +++ b/app/background-process/rpc-manifests/views.js @@ -30,6 +30,7 @@ export default { runLocationBarCmd: 'promise', showMenu: 'promise', toggleMenu: 'promise', + toggleSiteInfo: 'promise', focusShellWindow: 'promise', onFaviconLoadSuccess: 'promise', onFaviconLoadError: 'promise' diff --git a/app/background-process/ui/subwindows/site-info.js b/app/background-process/ui/subwindows/site-info.js new file mode 100644 index 0000000000..07a512f5eb --- /dev/null +++ b/app/background-process/ui/subwindows/site-info.js @@ -0,0 +1,138 @@ +/** + * Site Infos + * + * NOTES + * - There can only ever be one Site Info view for a given browser window + * - Site Info views are created with each browser window and then shown/hidden as needed + * - The Site Info view contains the UIs for multiple menus and swaps between them as needed + * - When unfocused, the Site Info view is hidden (it's meant to act as a popup menu) + */ + +import path from 'path' +import Events from 'events' +import { BrowserWindow, BrowserView } from 'electron' +import * as viewManager from '../view-manager' + +// globals +// = + +const MARGIN_SIZE = 10 +var events = new Events() +var views = {} // map of {[parentWindow.id] => BrowserView} + +// exported api +// = + +export function setup (parentWindow) { + var view = views[parentWindow.id] = new BrowserView({ + webPreferences: { + defaultEncoding: 'utf-8', + preload: path.join(__dirname, 'webview-preload.build.js') + } + }) + view.webContents.on('console-message', (e, level, message) => { + console.log('Site-Info window says:', message) + }) + view.webContents.loadURL('beaker://site-info/') +} + +export function destroy (parentWindow) { + if (get(parentWindow)) { + get(parentWindow).destroy() + delete views[parentWindow.id] + } +} + +export function get (parentWindow) { + return views[parentWindow.id] +} + +export function reposition (parentWindow) { + var view = get(parentWindow) + if (view) { + let parentBounds = parentWindow.getContentBounds() + const setBounds = (b) => { + // HACK workaround the lack of view.getBounds() -prf + if (view.currentBounds) { + b = view.currentBounds // use existing bounds + } + view.currentBounds = b // store new bounds + view.setBounds(adjustBounds(view, parentWindow, b)) + } + setBounds({ + x: view.boundsOpt ? view.boundsOpt.left : 0, + y: 72, + width: 400, + height: 350 + }) + } +} + +export async function toggle (parentWindow, opts) { + var view = get(parentWindow) + if (view) { + if (view.isVisible) { + return hide(parentWindow) + } else { + return show(parentWindow, opts) + } + } +} + +export async function show (parentWindow, opts) { + var view = get(parentWindow) + if (view) { + view.boundsOpt = opts && opts.bounds + parentWindow.addBrowserView(view) + reposition(parentWindow) + view.isVisible = true + + var params = opts && opts.params ? opts.params : {} + params.url = viewManager.getActive(parentWindow).url + await view.webContents.executeJavaScript(`init(${JSON.stringify(params)})`) + view.webContents.focus() + + // await till hidden + await new Promise(resolve => { + events.once('hide', resolve) + }) + } +} + +export function hide (parentWindow) { + var view = get(parentWindow) + if (view) { + view.webContents.executeJavaScript(`reset()`) + parentWindow.removeBrowserView(view) + view.currentBounds = null + view.isVisible = false + events.emit('hide') + } +} + +// internal methods +// = + +/** + * @description + * Ajust the bounds for margin + */ +function adjustBounds (view, parentWindow, bounds) { + let parentBounds = parentWindow.getContentBounds() + return { + x: bounds.x - MARGIN_SIZE, + y: bounds.y, + width: bounds.width + (MARGIN_SIZE * 2), + height: bounds.height + MARGIN_SIZE + } +} + +function getParentWindow (sender) { + var view = BrowserView.fromWebContents(sender) + for (let id in views) { + if (views[id] === view) { + return BrowserWindow.fromId(+id) + } + } + throw new Error('Parent window not found') +} \ No newline at end of file diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index baa3b2cfef..f097a8b398 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -21,6 +21,7 @@ import * as prompts from './subwindows/prompts' import * as permPrompt from './subwindows/perm-prompt' import * as modals from './subwindows/modals' import * as sidebars from './subwindows/sidebars' +import * as siteInfo from './subwindows/site-info' import * as windowMenu from './window-menu' import { getUserSessionFor } from './windows' import { getResourceContentType } from '../browser' @@ -1421,6 +1422,10 @@ rpc.exportAPI('background-process-views', viewsRPCManifest, { await shellMenus.toggle(getWindow(this.sender), id, opts) }, + async toggleSiteInfo (opts) { + await siteInfo.toggle(getWindow(this.sender), opts) + }, + async focusShellWindow () { getWindow(this.sender).webContents.focus() }, diff --git a/app/background-process/ui/windows.js b/app/background-process/ui/windows.js index b577848856..edad101147 100644 --- a/app/background-process/ui/windows.js +++ b/app/background-process/ui/windows.js @@ -20,6 +20,7 @@ import * as promptsSubwindow from './subwindows/prompts' import * as permPromptSubwindow from './subwindows/perm-prompt' import * as modalsSubwindow from './subwindows/modals' import * as sidebarsSubwindow from './subwindows/sidebars' +import * as siteInfoSubwindow from './subwindows/site-info' import { findWebContentsParentWindow } from '../../lib/electron' const settingsDb = beakerCore.dbs.settings @@ -31,7 +32,8 @@ const subwindows = { prompts: promptsSubwindow, permPrompt: permPromptSubwindow, modals: modalsSubwindow, - sidebars: sidebarsSubwindow + sidebars: sidebarsSubwindow, + siteInfo: siteInfoSubwindow } // globals diff --git a/app/new-shell-window/navbar/site-info.js b/app/new-shell-window/navbar/site-info.js index ebb7cea868..8b4282ac02 100644 --- a/app/new-shell-window/navbar/site-info.js +++ b/app/new-shell-window/navbar/site-info.js @@ -10,24 +10,26 @@ class NavbarSiteInfo extends LitElement { static get properties () { return { url: {type: String}, + siteIcon: {type: String}, siteTitle: {type: String}, datDomain: {type: String}, isOwner: {type: Boolean}, peers: {type: Number}, - numFollowers: {type: Number}, - loadError: {type: Object} + loadError: {type: Object}, + isPressed: {type: Boolean} } } - + constructor () { super() this.url = '' + this.siteIcon = '' this.siteTitle = '' this.datDomain = '' this.isOwner = false this.peers = 0 - this.numFollowers = 0 this.loadError = null + this.isPressed = false } get scheme () { @@ -57,7 +59,7 @@ class NavbarSiteInfo extends LitElement { const isInsecureResponse = _get(this, 'loadError.isInsecureResponse') if ((isHttps && !isInsecureResponse) || scheme === 'beaker:') { innerHTML = html` - + ${scheme !== 'beaker:' ? html`` : ''} ${this.siteTitle} ` } else if (scheme === 'http:') { @@ -72,15 +74,12 @@ class NavbarSiteInfo extends LitElement { ` } else if (scheme === 'dat:') { innerHTML = html` - - ${this.isOwner ? html` - Your Site - ` : ''} - ${this.siteTitle} - ${this.numFollowers > 0 ? html` - - ${this.numFollowers} - ` : ''} + ${this.isOwner ? html`My Site` : ''} + + + ${this.siteAuthor ? html`${this.siteAuthor} ` : ''} + ${this.siteTitle} + ` } } @@ -91,7 +90,7 @@ class NavbarSiteInfo extends LitElement { return html` - ` @@ -100,8 +99,16 @@ class NavbarSiteInfo extends LitElement { // events // = - async onClickButton () { - bg.views.toggleSidebar('active', 'site') + async onClickButton (e) { + this.isPressed = true + var rect = e.currentTarget.getClientRects()[0] + await bg.views.toggleSiteInfo({ + bounds: { + top: (rect.bottom|0), + left: (rect.left|0) + } + }) + this.isPressed = false } } NavbarSiteInfo.styles = [buttonResetCSS, css` @@ -110,11 +117,10 @@ NavbarSiteInfo.styles = [buttonResetCSS, css` } button { - border-right: 1px solid #ccc; border-radius: 0; height: 26px; line-height: 26px; - padding: 0 10px; + padding: 0 4px 0 10px; } button:hover { diff --git a/app/prompts/create-page.js b/app/prompts/create-page.js index 6d28a9237b..2f059c1fde 100644 --- a/app/prompts/create-page.js +++ b/app/prompts/create-page.js @@ -29,11 +29,10 @@ class CreatePagePrompt extends LitElement { } async init (params) { - this.url = params.url + this.url = params.url await this.requestUpdate() } - // rendering // = From 51f2229dd9dc3e7ec1dc18ceaa580bfabec5796d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 18 Sep 2019 10:10:29 -0500 Subject: [PATCH 0028/1212] Add beaker://site-info, beaker://compare, and beaker://viewers --- app/background-process/protocols/beaker.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/background-process/protocols/beaker.js b/app/background-process/protocols/beaker.js index 1027c6a52b..2a9b919d29 100644 --- a/app/background-process/protocols/beaker.js +++ b/app/background-process/protocols/beaker.js @@ -13,8 +13,11 @@ import ICO from 'icojs' const START_APP_PATH = path.dirname(require.resolve('@beaker/start-app')).replace('app.asar', 'app.asar.unpacked') const LIBRARY_APP_PATH = path.dirname(require.resolve('@beaker/library-app')).replace('app.asar', 'app.asar.unpacked') +const COMPARE_APP_PATH = path.dirname(require.resolve('@beaker/compare-app')).replace('app.asar', 'app.asar.unpacked') const SEARCH_APP_PATH = path.dirname(require.resolve('@beaker/search-app')).replace('app.asar', 'app.asar.unpacked') const SIDEBAR_APP_PATH = path.dirname(require.resolve('@beaker/sidebar-app')).replace('app.asar', 'app.asar.unpacked') +const SITE_INFO_APP_PATH = path.dirname(require.resolve('@beaker/site-info-app')).replace('app.asar', 'app.asar.unpacked') +const VIEWER_APPS_PATH = path.dirname(require.resolve('@beaker/viewer-apps')).replace('app.asar', 'app.asar.unpacked') // constants // = @@ -204,9 +207,18 @@ async function beakerProtocol (request, respond) { if (requestUrl === 'beaker://library' || requestUrl.startsWith('beaker://library/')) { return serveAppAsset(requestUrl, LIBRARY_APP_PATH, cb) } + if (requestUrl === 'beaker://compare' || requestUrl.startsWith('beaker://compare/')) { + return serveAppAsset(requestUrl, COMPARE_APP_PATH, cb) + } if (requestUrl === 'beaker://search' || requestUrl.startsWith('beaker://search/')) { return serveAppAsset(requestUrl, SEARCH_APP_PATH, cb) } + if (requestUrl === 'beaker://site-info' || requestUrl.startsWith('beaker://site-info/')) { + return serveAppAsset(requestUrl, SITE_INFO_APP_PATH, cb, {fallbackToIndexHTML: true}) + } + if (requestUrl === 'beaker://viewers' || requestUrl.startsWith('beaker://viewers/')) { + return serveAppAsset(requestUrl, VIEWER_APPS_PATH, cb) + } if (requestUrl === 'beaker://history/') { return cb(200, 'OK', 'text/html; charset=utf-8', path.join(__dirname, 'builtin-pages/history.html')) } From b7c148c89234c5962587c675544b4ac6dfa3bfb0 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 18 Sep 2019 10:10:47 -0500 Subject: [PATCH 0029/1212] Update default context menu --- app/background-process/ui/context-menu.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/background-process/ui/context-menu.js b/app/background-process/ui/context-menu.js index 690d8742c9..871326a8e8 100644 --- a/app/background-process/ui/context-menu.js +++ b/app/background-process/ui/context-menu.js @@ -120,13 +120,6 @@ export default function registerContextMenu () { } if (!props.linkURL && props.mediaType === 'none' && !hasText) { - menuItems.push({ - label: 'About This Site', - click: (item, win) => { - viewManager.getActive(win).toggleSidebar('site') - } - }) - menuItems.push({ type: 'separator' }) menuItems.push({ label: 'Back', enabled: webContents.canGoBack(), @@ -154,7 +147,7 @@ export default function registerContextMenu () { } menuItems.push({ - label: 'Edit Source', + label: 'Edit Page', click: (item, win) => { viewManager.getActive(win).toggleSidebar('editor') } From f9437c8b076133f7c7f0ba04afdce2acde48a2e4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 18 Sep 2019 10:13:39 -0500 Subject: [PATCH 0030/1212] Change start-page to beaker://library --- app/background-process/ui/default-state.js | 2 +- app/background-process/ui/view-manager.js | 2 +- app/location-bar.js | 2 +- app/new-shell-window/navbar.js | 2 +- app/new-shell-window/navbar/location.js | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/background-process/ui/default-state.js b/app/background-process/ui/default-state.js index 6f77715b11..9e7de0589b 100644 --- a/app/background-process/ui/default-state.js +++ b/app/background-process/ui/default-state.js @@ -30,5 +30,5 @@ export function defaultWindowState () { } export function defaultPageState () { - return [ 'beaker://start' ] + return [ 'beaker://library' ] } diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index f097a8b398..d44b0c1828 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -67,7 +67,7 @@ const TLS_ERROR_CODES = Object.values({ const IS_CODE_INSECURE_RESPONSE = x => x === ERR_CONNECTION_REFUSED || x === ERR_INSECURE_RESPONSE || (x <= -200 && x > -300) || TLS_ERROR_CODES.includes(x) const Y_POSITION = 76 -const DEFAULT_URL = 'beaker://start' +const DEFAULT_URL = 'beaker://library' const TRIGGER_LIVE_RELOAD_DEBOUNCE = 500 // throttle live-reload triggers by this amount // the variables which are automatically sent to the shell-window for rendering diff --git a/app/location-bar.js b/app/location-bar.js index 3f30f34fec..5b154fb81e 100644 --- a/app/location-bar.js +++ b/app/location-bar.js @@ -207,7 +207,7 @@ class LocationBar extends LitElement { if (opts.value && opts.value !== this.inputValue) { this.inputQuery = this.inputValue = opts.value this.currentSelection = 0 - if (this.inputValue.startsWith('beaker://start')) { + if (this.inputValue.startsWith('beaker://library')) { this.inputQuery = this.inputValue = '' } diff --git a/app/new-shell-window/navbar.js b/app/new-shell-window/navbar.js index 973b1ebff3..880100ce01 100644 --- a/app/new-shell-window/navbar.js +++ b/app/new-shell-window/navbar.js @@ -241,7 +241,7 @@ class ShellWindowNavbar extends LitElement { } onClickHome (e) { - bg.views.loadURL('active', 'beaker://start/') + bg.views.loadURL('active', 'beaker://library/') } onClickWatchlistBtn (e) { diff --git a/app/new-shell-window/navbar/location.js b/app/new-shell-window/navbar/location.js index 85307fddf4..44e03f4416 100644 --- a/app/new-shell-window/navbar/location.js +++ b/app/new-shell-window/navbar/location.js @@ -130,9 +130,9 @@ class NavbarLocation extends LitElement { } renderInputPretty () { - if (this.url.startsWith('beaker://start')) { + if (this.url.startsWith('beaker://library')) { return html` -
+
Search or enter your address here
` From 126aafd94874a03a229485085d8c1f5bd289c597 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 18 Sep 2019 10:15:43 -0500 Subject: [PATCH 0031/1212] Fixes for prompts creation/removal and positioning --- .../ui/subwindows/prompts.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/background-process/ui/subwindows/prompts.js b/app/background-process/ui/subwindows/prompts.js index 1336d6e60e..381af46b6c 100644 --- a/app/background-process/ui/subwindows/prompts.js +++ b/app/background-process/ui/subwindows/prompts.js @@ -28,19 +28,19 @@ export function setup (parentWindow) { export function destroy (parentWindow) { // destroy all under this window - for (let view of viewManager.getAll(parentWindow)) { - if (view.id in views) { - views[view.id].destroy() - delete views[view.id] + for (let parentView of viewManager.getAll(parentWindow)) { + if (parentView.id in views) { + views[parentView.id].destroy() + delete views[parentView.id] } } } export function reposition (parentWindow) { // reposition all under this window - for (let view of viewManager.getAll(parentWindow)) { - if (view.id in views) { - setBounds(views[view.id], parentWindow) + for (let parentView of viewManager.getAll(parentWindow)) { + if (parentView.id in views) { + setBounds(views[parentView.id], parentView, parentWindow) } } } @@ -52,6 +52,7 @@ export async function create (webContents, promptName, params = {}) { if (parentView && !parentWindow) { // if there's no window, then a web page or "sub-window" created the prompt // use its containing window + parentView = viewManager.findView(parentView) parentWindow = findWebContentsParentWindow(parentView.webContents) } else if (!parentView) { // if there's no view, then the shell window created the prompt @@ -76,7 +77,7 @@ export async function create (webContents, promptName, params = {}) { if (viewManager.getActive(parentWindow).id === parentView.id) { parentWindow.addBrowserView(view) } - setBounds(view, parentWindow) + setBounds(view, parentView, parentWindow) view.webContents.on('console-message', (e, level, message) => { console.log('Prompts window says:', message) }) @@ -95,7 +96,7 @@ export function show (parentView) { if (!win) win = findWebContentsParentWindow(view.webContents) if (win) { win.addBrowserView(view) - setBounds(view, win) + setBounds(view, parentView, win) } } } @@ -154,12 +155,12 @@ function getDefaultHeight (view) { return 80 } -function setBounds (view, parentWindow, {width, height} = {}) { +function setBounds (view, parentView, parentWindow, {width, height} = {}) { var parentBounds = parentWindow.getContentBounds() width = Math.min(width || getDefaultWidth(view), parentBounds.width - 20) height = Math.min(height || getDefaultHeight(view), parentBounds.height - 20) view.setBounds({ - x: 0, + x: parentView.isSidebarActive ? Math.floor(parentBounds.width / 2) : 0, y: 85, width: width + (MARGIN_SIZE * 2), height: height + MARGIN_SIZE From 1851da181af0c7a5379f378e9e9c4ff2eb2f2b37 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 18 Sep 2019 10:18:21 -0500 Subject: [PATCH 0032/1212] Updates to browsing UI --- .../ui/subwindows/shell-menus.js | 11 +- .../ui/subwindows/sidebars.js | 2 - app/background-process/ui/view-manager.js | 25 ++- app/new-shell-window/navbar.js | 48 ++--- app/new-shell-window/navbar/location.js | 119 +++++------- app/shell-menus.js | 1 - app/shell-menus/site-tools.js | 182 ------------------ app/shell-menus/users.js | 3 +- 8 files changed, 91 insertions(+), 300 deletions(-) delete mode 100644 app/shell-menus/site-tools.js diff --git a/app/background-process/ui/subwindows/shell-menus.js b/app/background-process/ui/subwindows/shell-menus.js index f79e179eca..a1e53b5676 100644 --- a/app/background-process/ui/subwindows/shell-menus.js +++ b/app/background-process/ui/subwindows/shell-menus.js @@ -21,7 +21,7 @@ import shellMenusRPCManifest from '../../rpc-manifests/shell-menus' // = const MARGIN_SIZE = 10 -const IS_RIGHT_ALIGNED = ['browser', 'users', 'bookmark', 'donate', 'site-tools'] +const IS_RIGHT_ALIGNED = ['browser', 'users', 'bookmark', 'donate'] var events = new Events() var views = {} // map of {[parentWindow.id] => BrowserView} @@ -75,7 +75,7 @@ export function reposition (parentWindow) { setBounds({ x: parentBounds.width - view.boundsOpt.right, y: 72, - width: 250, + width: 220, height: 350 }) } else if (view.menuId === 'bookmark') { @@ -92,13 +92,6 @@ export function reposition (parentWindow) { width: 350, height: 90 }) - } else if (view.menuId === 'site-tools') { - setBounds({ - x: parentBounds.width - view.boundsOpt.left, - y: view.boundsOpt.top, - width: 220, - height: 152 - }) } } } diff --git a/app/background-process/ui/subwindows/sidebars.js b/app/background-process/ui/subwindows/sidebars.js index 7b511e5505..c09a7bb3f6 100644 --- a/app/background-process/ui/subwindows/sidebars.js +++ b/app/background-process/ui/subwindows/sidebars.js @@ -9,9 +9,7 @@ import path from 'path' import { app, BrowserWindow, BrowserView } from 'electron' -import * as rpc from 'pauls-electron-rpc' import * as viewManager from '../view-manager' -// import modalsRPCManifest from '../../rpc-manifests/modals' import { findWebContentsParentWindow } from '../../../lib/electron' // globals diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index d44b0c1828..2c1aeb123b 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -1,6 +1,7 @@ import { app, dialog, BrowserView, BrowserWindow, Menu, clipboard, ipcMain } from 'electron' import * as beakerCore from '@beaker/core' import errorPage from '@beaker/core/lib/error-page' +import * as libTools from '@beaker/library-tools' import path from 'path' import { promises as fs } from 'fs' import Events from 'events' @@ -75,6 +76,7 @@ const STATE_VARS = [ 'url', 'title', 'siteTitle', + 'siteIcon', 'datDomain', 'isOwner', 'numFollowers', @@ -216,19 +218,19 @@ class View { try { var hostname = ((parseDatURL(this.url)).hostname).replace(/\+(.+)$/, '') if (this.datInfo) { + var userSession = getUserSessionFor(this.browserWindow.webContents) + if (userSession && userSession.url === this.datInfo.url) { + return 'My Profile' + } if (this.datInfo.domain) { - // use confirmed domain if available (because we give a checkmark for that) + // use confirmed domain if available return this.datInfo.domain } - if ((this.datInfo.title || '').trim()) { - // use site title if it exists - return (this.datInfo.title || '').trim() - } // pretty hash of the key otherwise return prettyHash(this.datInfo.key) } if (this.url.startsWith('beaker://')) { - return `Beaker ${ucfirst(hostname)}` + return 'Beaker' } return hostname } catch (e) { @@ -236,6 +238,17 @@ class View { } } + get siteIcon () { + if (this.datInfo) { + var userSession = getUserSessionFor(this.browserWindow.webContents) + if (userSession && userSession.url === this.datInfo.url) { + return 'far fa-user-circle' + } + return libTools.getFAIcon(libTools.typeToCategory(this.datInfo.type)) + } + return '' + } + get datDomain () { return this.datInfo && this.datInfo.domain ? this.datInfo.domain : '' } diff --git a/app/new-shell-window/navbar.js b/app/new-shell-window/navbar.js index 880100ce01..cb21b47fd8 100644 --- a/app/new-shell-window/navbar.js +++ b/app/new-shell-window/navbar.js @@ -61,12 +61,12 @@ class ShellWindowNavbar extends LitElement { .activeTabIndex="${this.activeTabIndex}" url="${_get(this, 'activeTab.url', '')}" title="${_get(this, 'activeTab.title', '')}" + siteIcon="${_get(this, 'activeTab.siteIcon', '')}" siteTitle="${_get(this, 'activeTab.siteTitle', '')}" datDomain="${_get(this, 'activeTab.datDomain', '')}" ?isOwner="${_get(this, 'activeTab.isOwner', false)}" peers="${_get(this, 'activeTab.peers', 0)}" numFollowers="${_get(this, 'activeTab.numFollowers', 0)}" - numComments="${_get(this, 'activeTab.numComments', 0)}" zoom="${_get(this, 'activeTab.zoom', '')}" .loadError=${_get(this, 'activeTab.loadError', null)} donate-link-href="${_get(this, 'activeTab.donateLinkHref') || ''}" @@ -81,9 +81,11 @@ class ShellWindowNavbar extends LitElement { active-match="${_get(this, 'activeTab.currentInpageFindResults.activeMatchOrdinal', '0')}" num-matches="${_get(this, 'activeTab.currentInpageFindResults.matches', '0')}" > -
+
${this.watchlistBtn} - ${this.openTerminalBtn} + ${this.usersMenuBtn} ${this.browserMenuBtn}
@@ -92,7 +94,7 @@ class ShellWindowNavbar extends LitElement { get backBtn () { return html` - @@ -196,7 +198,7 @@ class ShellWindowNavbar extends LitElement { get browserMenuBtn () { const cls = classMap({pressed: this.isBrowserMenuOpen}) return html` - - ` - } - get usersMenuBtn () { const cls = classMap({'users-btn': true, pressed: this.isUsersMenuOpen}) return html` - ` @@ -258,11 +252,7 @@ class ShellWindowNavbar extends LitElement { right: (rect.right|0) } }) - this.isUsersMenuOpen = false - } - - onClickOpenTerminal (e) { - bg.views.toggleSidebar('active', 'terminal') + this.isUsersMenuOpen = false } async onClickBrowserMenu (e) { @@ -294,8 +284,8 @@ button .fas { color: #333; } -button .fas.fa-plus { - -webkit-text-stroke: 0.8px #eee; +button .far.fa-hdd { + color: rgba(0,0,0,.6); } svg.icon * { @@ -311,8 +301,10 @@ svg.icon.refresh { padding: 0 8px; } -button .fa-terminal { - font-size: 14px; +button .fa-cloud { + font-size: 13px; + -webkit-text-stroke: 1.5px black; + color: #d8d6d6; } .fa-arrow-alt-circle-up { diff --git a/app/new-shell-window/navbar/location.js b/app/new-shell-window/navbar/location.js index 44e03f4416..276d562ac7 100644 --- a/app/new-shell-window/navbar/location.js +++ b/app/new-shell-window/navbar/location.js @@ -18,11 +18,11 @@ class NavbarLocation extends LitElement { url: {type: String}, title: {type: String}, siteTitle: {type: String}, + siteIcon: {type: String}, datDomain: {type: String}, isOwner: {type: Boolean}, peers: {type: Number}, numFollowers: {type: Number}, - numComments: {type: Number}, zoom: {type: Number}, loadError: {type: Object}, donateLinkHref: {type: String, attribute: 'donate-link-href'}, @@ -40,11 +40,11 @@ class NavbarLocation extends LitElement { this.url = '' this.title = '' this.siteTitle = '' + this.siteIcon = '' this.datDomain = '' this.isOwner = false this.peers = 0 this.numFollowers = 0 - this.numComments = 0 this.zoom = 0 this.loadError = null this.donateLinkHref = false @@ -57,6 +57,10 @@ class NavbarLocation extends LitElement { ipcRenderer.on('command', this.onCommand.bind(this)) } + get isBeaker () { + return this.url.startsWith('beaker://') + } + get isDat () { return this.url.startsWith('dat://') } @@ -65,6 +69,7 @@ class NavbarLocation extends LitElement { var input = this.shadowRoot.querySelector('.input-container input') input.focus() bg.views.focusShellWindow() // focus the shell-window UI + input.setSelectionRange(0, input.value.length) } unfocusLocation () { @@ -81,6 +86,7 @@ class NavbarLocation extends LitElement { +
${this.renderLocation()} ${this.renderZoom()} ${this.renderLiveReloadingBtn()} + ${this.renderAvailableAlternativeBtn()} + ${this.renderDonateBtn()} ${this.isDat ? html` - + +
` : ''} - - ${this.renderAvailableAlternativeBtn()} - ${this.renderDonateBtn()} ${this.renderBookmarkBtn()} ` } renderLocation () { var url = this.url - if (url.startsWith('beaker://start')) { + if (url.startsWith('beaker://library')) { url = '' } return html` -
+
- ${protocol.slice(0, -1)}://${host}${hostVersion ? html`${hostVersion}` : ''}${pathname}${search}${hash} +
+ ${pathname}${search}${hash}
` } catch (e) { @@ -173,7 +178,7 @@ class NavbarLocation extends LitElement { } } return html` -
${this.url}
+
${this.url}
` } @@ -253,7 +258,7 @@ class NavbarLocation extends LitElement { 'fa-star': true }) return html` - ` @@ -275,46 +280,18 @@ class NavbarLocation extends LitElement { } onContextMenuLocation (e) { + e.preventDefault() + this.focusLocation() bg.views.showLocationBarContextMenu('active') } - onMousedownLocation (e) { - // track if the user is clicking, doubleclicking, or dragging the location before its focused - // if a click, select all; if a doubleclick, select word under cursor; if a drag, do default behavior - if (!this.isLocationFocused) { - this.lastMousedownLocationTs = Date.now() - } - } - - onMouseupLocation (e) { - if (Date.now() - this.lastMousedownLocationTs <= 300) { - // was a fast click (probably not a drag) so select all - e.preventDefault() - let inputEl = e.currentTarget - this.mouseupClickIndex = inputEl.selectionStart - inputEl.select() - - // setup double-click override - this.lastMousedownLocationTs = 0 - this.lastMouseupLocationTs = Date.now() - } - } - - onDblclickLocation (e) { - if (Date.now() - this.lastMouseupLocationTs <= 300) { - e.preventDefault() - - // select the text under the cursor - // (we have to do this manually because we previously selected all on mouseup, which f's that default behavior up) - let inputEl = e.currentTarget - let {start, end} = findWordBoundary(inputEl.value, this.mouseupClickIndex) - inputEl.setSelectionRange(start, end) - this.lastMouseupLocationTs = 0 - } + onClickLocation (e) { + e.preventDefault() + this.focusLocation() } onFocusLocation (e) { - e.currentTarget.value = this.url.startsWith('beaker://start') ? '' : this.url + e.currentTarget.value = this.url.startsWith('beaker://library') ? '' : this.url e.currentTarget.setSelectionRange(0, this.url.length) this.isLocationFocused = true } @@ -340,12 +317,8 @@ class NavbarLocation extends LitElement { e.currentTarget.blur() } - onClickEdit (e) { - bg.views.toggleSidebar('active', 'editor') - } - - onClickComments (e) { - bg.views.toggleSidebar('active', 'comments') + onClickFollow (e) { + // TODO } onClickZoom (e) { @@ -417,18 +390,27 @@ NavbarLocation.styles = [buttonResetCSS, css` background: var(--bg-input); border: 1px solid var(--color-border-input); border-radius: 4px; + padding-right: 2px; +} + +hr { + width: 0; + margin: 5px; + border: 0; + border-left: 1px solid var(--color-border-input); } button { width: 27px; border-radius: 0; color: #666; + margin: 0 2px; } button.text { width: auto; padding: 0 4px; - font-size: 12px; + font-size: 11px; } button .fa, @@ -442,14 +424,8 @@ button.text .far { font-size: 13px; } -button.text .fa-edit { - position: relative; - top: -1px; - left: 1px; -} - -button.text .fa-comment-alt { - font-size: 12px; +button.text .fa-info-circle { + font-size: 14px; } button .fa-star { @@ -460,6 +436,10 @@ button .fas.fa-star { color: #f3cc00; } +button .fa-terminal { + font-size: 13px; +} + button.zoom { width: auto; font-size: 11px; @@ -495,7 +475,7 @@ button.live-reload .fa { .input-container { position: relative; flex: 1; - margin: 0 8px; + margin: 0 6px; } .input-pretty, @@ -533,7 +513,6 @@ input::-webkit-input-placeholder { z-index: 1; text-overflow: ellipsis; cursor: text; - pointer-events: none; } .input-pretty .protocol { diff --git a/app/shell-menus.js b/app/shell-menus.js index 3ba8327188..e105bbfedc 100644 --- a/app/shell-menus.js +++ b/app/shell-menus.js @@ -5,7 +5,6 @@ import './shell-menus/browser' import './shell-menus/users' import './shell-menus/bookmark' import './shell-menus/donate' -import './shell-menus/site-tools' class MenusWrapper extends LitElement { static get properties () { diff --git a/app/shell-menus/site-tools.js b/app/shell-menus/site-tools.js deleted file mode 100644 index e24eed4a64..0000000000 --- a/app/shell-menus/site-tools.js +++ /dev/null @@ -1,182 +0,0 @@ -/* globals customElements */ -import { LitElement, html, css } from '../vendor/lit-element/lit-element' -import _get from 'lodash.get' -import * as bg from './bg-process-rpc' -import commonCSS from './common.css' - -class SiteToolsMenu extends LitElement { - static get properties () { - return { - submenu: {type: String} - } - } - - constructor () { - super() - this.reset() - } - - reset () { - this.tabState = null - this.submenu = '' - } - - get datInfo () { - if (!this.tabState) return null - return this.tabState.datInfo - } - - get isDat () { - return !!this.datInfo - } - - get isSaved () { - return this.datInfo && this.datInfo.userSettings && this.datInfo.userSettings.isSaved - } - - async init (params) { - this.tabState = await bg.views.getTabState('active', {datInfo: true}) - await this.requestUpdate() - } - - // rendering - // = - - render () { - if (this.submenu === 'devtools') { - return html` - -
-
- -

Developer tools

-
-
- - -
- ` - } - return html` - -
- ${this.isDat ? html` - ${this.isSaved ? html` - - ` : html` - - `} - -
- -
- - ` : ''} - - -
- ` - } - - // events - // = - - updated () { - // adjust height based on rendering - var height = this.shadowRoot.querySelector('div').clientHeight - bg.shellMenus.resizeSelf({height}) - } - - onShowSubmenu (v) { - this.submenu = v - } - - async onToggleSaved () { - if (this.isSaved) { - await bg.archives.remove(this.datInfo.url) - } else { - await bg.archives.add(this.datInfo.url) - } - bg.shellMenus.close() - } - - async onClickViewFiles () { - await bg.shellMenus.createTab(`beaker://library/?view=files&dat=${encodeURIComponent(`dat://${this.datInfo.key}`)}`) - bg.shellMenus.close() - } - - async onClickViewSource () { - await bg.views.toggleSidebar('active', 'editor') - bg.shellMenus.close() - } - - async onClickFork () { - const forkUrl = await bg.datArchive.forkArchive(this.datInfo.key, {prompt: true}).catch(() => false) - if (forkUrl) { - bg.shellMenus.loadURL(`beaker://editor/${forkUrl}`) - } - bg.shellMenus.close() - } - - async onToggleLiveReloading () { - await bg.views.toggleLiveReloading('active') - bg.shellMenus.close() - } - - async onClickDownloadZip () { - bg.beakerBrowser.downloadURL(`dat://${this.datInfo.key}?download_as=zip`) - bg.shellMenus.close() - } - - async onToggleDevtools () { - await bg.views.toggleDevTools('active') - bg.shellMenus.close() - } - - async onClickSavePage () { - bg.beakerBrowser.downloadURL(this.tabState.url) - bg.shellMenus.close() - } - - async onClickPrint () { - bg.views.print('active') - bg.shellMenus.close() - } -} -SiteToolsMenu.styles = [commonCSS, css` -.wrapper { - padding: 4px 0; -} -`] - -customElements.define('site-tools-menu', SiteToolsMenu) diff --git a/app/shell-menus/users.js b/app/shell-menus/users.js index cf6561e0a1..1c4fe1fcee 100644 --- a/app/shell-menus/users.js +++ b/app/shell-menus/users.js @@ -26,7 +26,6 @@ class UsersMenu extends LitElement { async init (params) { this.user = await bg.users.getCurrent().catch(err => undefined) this.users = await bg.users.list() - console.log(this.user, this.users) await this.requestUpdate() } @@ -52,7 +51,7 @@ class UsersMenu extends LitElement {
- @@ -160,12 +160,12 @@ class BrowserMenu extends LitElement {
- -
${toNiceUrl(r.urlDecorated ? unsafeHTML(r.urlDecorated) : r.url)}
+
+ + ${toNiceUrl(r.urlDecorated ? unsafeHTML(r.urlDecorated) : r.url)} +
` } @@ -303,9 +331,12 @@ class LocationBar extends LitElement { var finalResults var [crawlerResults, historyResults] = await Promise.all([ - bg.search.query({query: this.inputValue, filters: {datasets: ['sites', 'unwalled.garden/bookmark']}, limit: 10}), - bg.history.search(this.inputValue) + bg.search.query({query: this.inputValue, limit: 10}), + []// DISABLED bg.history.search(this.inputValue) ]) + // History search is experimentally disabled + // We're seeing how it feels to focus entirely on results from the network + // -prf // console.log({ // historyResults, @@ -422,7 +453,8 @@ input:focus { } .result .icon .fa, -.result .icon .fas { +.result .icon .fas, +.result .icon .far { font-size: 13px; color: #707070; margin-left: 9px; @@ -433,7 +465,6 @@ input:focus { font-size: 15px; } -.result .url-column, .result .content-column, .result .search-column { overflow: hidden; @@ -442,10 +473,6 @@ input:focus { max-width: 100%; } -.result .url-column { - color: #707070; -} - .result .title, .result .description { color: #1f55c1; @@ -464,7 +491,8 @@ input:focus { color: #555; } -.provenance .fas { +.provenance .fas, +.provenance .far { font-size: 11px; position: relative; top: -1px; @@ -476,11 +504,6 @@ input:focus { margin-left: 5px; } -.is-you { - color: #3a3d4e; - font-weight: 500; -} - .result.selected { background: #dbebff; } @@ -560,16 +583,9 @@ function highlightSearchResult (searchTerms, nonce, result) { var termRe = new RegExp(`(${searchTerms.join('|')})`, 'gi') // eg '(beaker|browser)' const highlight = str => makeSafe(str).replace(start, '').replace(end, '').replace(termRe, (_, term) => `${term}`) - if (result.record.type === 'site') { - result.titleDecorated = highlight(result.title) - result.descriptionDecorated = highlight(result.description) - } else if (result.record.type === 'unwalled.garden/bookmark') { - result.url = result.content.href - result.titleDecorated = highlight(result.content.title) - result.descriptionDecorated = '' - if (result.content.description) result.descriptionDecorated += highlight(result.content.description) - if (result.content.tags && result.content.tags.filter(Boolean).length) result.descriptionDecorated += `(${highlight(result.content.tags.join(' '))})` - } + result.url = result.record.href || result.href + result.titleDecorated = highlight(result.record.title || result.record.body) + result.descriptionDecorated = highlight(result.record.description) } // helper for history search results From 548f2c184cdfc362db071df5ec1cd88c65af5efe Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 25 Sep 2019 21:15:56 -0500 Subject: [PATCH 0050/1212] Improve screenshot framing --- app/background-process/ui/view-manager.js | 46 +++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index fe66493c50..5b98a83cd5 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -426,13 +426,16 @@ class View { var orgsize = image.getSize() var bounds = findImageBounds(image.toBitmap(), orgsize) - // adjust the bounds to match the 100x80 aspect ratio - if (bounds.width < bounds.height) { - // adjust width - bounds.right = bounds.left + (bounds.height / 0.8)|0 - } else { - // adjust height - bounds.bottom = bounds.top + (bounds.width * 0.8)|0 + // set a minimum size of 200x160 + if (bounds.width < 200) { + let incDiv2 = ((200 - bounds.width) / 2)|0 + bounds.left -= incDiv2 + bounds.right += incDiv2 + } + if (bounds.height < 160) { + let incDiv2 = ((160 - bounds.height) / 2)|0 + bounds.top -= incDiv2 + bounds.bottom += incDiv2 } // give some margin @@ -441,6 +444,35 @@ class View { bounds.top = Math.max(0, bounds.top - 20) bounds.bottom = Math.min(orgsize.height, bounds.bottom + 20) + // adjust the bounds to match the 100x80 aspect ratio + if (bounds.width < bounds.height) { + bounds.right = bounds.left + (bounds.height / 0.8)|0 + } else { + bounds.bottom = bounds.top + (bounds.width * 0.8)|0 + } + + // ensure the bounds are still within the viewport + if (bounds.left < 0) { + let inc = 0 - bounds.left + bounds.left += inc + bounds.right += inc + } + if (bounds.right > orgsize.width) { + let inc = orgsize.width - bounds.right + bounds.left -= inc + bounds.right -= inc + } + if (bounds.top < 0) { + let inc = 0 - bounds.top + bounds.top += inc + bounds.bottom += inc + } + if (bounds.bottom > orgsize.height) { + let inc = orgsize.height - bounds.bottom + bounds.top -= inc + bounds.bottom -= inc + } + image = image .crop(bounds) .resize({width: 200, height: 160}) From eee5fcc5f92069b978e9291bbc77dbf55c4c7835 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 26 Sep 2019 10:03:44 -0500 Subject: [PATCH 0051/1212] Edit Page -> View Source --- app/background-process/ui/context-menu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/background-process/ui/context-menu.js b/app/background-process/ui/context-menu.js index 871326a8e8..d9019da0e0 100644 --- a/app/background-process/ui/context-menu.js +++ b/app/background-process/ui/context-menu.js @@ -147,7 +147,7 @@ export default function registerContextMenu () { } menuItems.push({ - label: 'Edit Page', + label: 'View Source', click: (item, win) => { viewManager.getActive(win).toggleSidebar('editor') } From 3ebb5de9397a6271fecfa6fe774a6fc298b5b803 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Sat, 28 Sep 2019 10:56:49 -0500 Subject: [PATCH 0052/1212] Much lighter UI and some progress on the cert & location-bar contextual actions --- app/assets/img/favicons/news-feed.png | Bin 0 -> 223 bytes app/background-process/protocols/asset.js | 2 +- app/background-process/ui/view-manager.js | 71 +++++++++--- app/new-shell-window/bg-process-rpc.js | 4 + app/new-shell-window/navbar.js | 11 +- app/new-shell-window/navbar/location.js | 85 +++++++++----- app/new-shell-window/navbar/site-info.js | 60 ++++++---- app/new-shell-window/tabs.js | 135 ++++++++-------------- app/shell-window.html | 24 ++-- 9 files changed, 230 insertions(+), 162 deletions(-) create mode 100644 app/assets/img/favicons/news-feed.png diff --git a/app/assets/img/favicons/news-feed.png b/app/assets/img/favicons/news-feed.png new file mode 100644 index 0000000000000000000000000000000000000000..9d75d5d85ddc6005bee93d0d2e99b2957952a7fa GIT binary patch literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)I6a#!hT!FNSXB-}6!&~SCRK!&h z { if (buf) cb({mimeType: 'image/png', data: buf}) diff --git a/app/background-process/ui/view-manager.js b/app/background-process/ui/view-manager.js index 5b98a83cd5..d3874dce2c 100644 --- a/app/background-process/ui/view-manager.js +++ b/app/background-process/ui/view-manager.js @@ -8,7 +8,6 @@ import Events from 'events' import _throttle from 'lodash.throttle' import parseDatURL from 'parse-dat-url' import emitStream from 'emit-stream' -import prettyHash from 'pretty-hash' import _get from 'lodash.get' import _pick from 'lodash.pick' import * as rpc from 'pauls-electron-rpc' @@ -79,7 +78,12 @@ const STATE_VARS = [ 'siteIcon', 'datDomain', 'isOwner', + 'canFollow', + 'isFollowing', 'numFollowers', + 'canSave', + 'isSaved', + 'isMyProfile', 'numComments', 'peers', 'favicons', @@ -157,8 +161,11 @@ class View { this.peers = 0 // how many peers does the site have? this.isBookmarked = false // is the active page bookmarked? this.datInfo = null // metadata about the site if viewing a dat + this.isSystemDat = undefined // is this the root drive or a user? + this.isFollowing = undefined // is the current user following this drive? this.numFollowers = 0 // how many sites are following this site? (unwalled garden) this.numComments = 0 // how many comments are on the current page? (unwalled garden) + this.confirmedAuthorTitle = undefined // the title of the confirmed author of the site this.donateLinkHref = null // the URL of the donate site, if set by the dat.json this.availableAlternative = '' // tracks if there's alternative protocol available for the site this.wasDatTimeout = false // did the last navigation result in a timed-out dat? @@ -219,19 +226,12 @@ class View { var urlp = parseDatURL(this.url) var hostname = (urlp.hostname).replace(/\+(.+)$/, '') if (this.datInfo) { - var userSession = getUserSessionFor(this.browserWindow.webContents) - if (userSession && userSession.url === this.datInfo.url) { - return 'My Profile' + if (this.datInfo.type === 'unwalled.garden/person') { + return this.datInfo.title || 'Anonymous' } - if (this.datInfo.domain) { - // use confirmed domain if available - return this.datInfo.domain - } - // pretty hash of the key otherwise - return prettyHash(this.datInfo.key) - } - if (urlp.protocol === 'dat:') { - return DAT_KEY_REGEX.test(hostname) ? prettyHash(hostname) : '' + return `"${this.datInfo.title || 'Untitled'}" by ${this.confirmedAuthorTitle ? this.confirmedAuthorTitle : '(Unknown)'}` + } else if (urlp.protocol === 'dat:') { + return '(Untitled) by (Unknown)' } if (urlp.protocol === 'beaker:') { return 'Beaker' @@ -244,10 +244,6 @@ class View { get siteIcon () { if (this.datInfo) { - var userSession = getUserSessionFor(this.browserWindow.webContents) - if (userSession && userSession.url === this.datInfo.url) { - return 'far fa-user-circle' - } return libTools.getFAIcon(libTools.typeToCategory(this.datInfo.type)) } return '' @@ -261,6 +257,23 @@ class View { return this.datInfo && this.datInfo.isOwner } + get canFollow () { + return this.datInfo && this.datInfo.type === 'unwalled.garden/person' && !this.isMyProfile + } + + get canSave () { + return this.datInfo && !this.isSystemDat + } + + get isSaved () { + return this.datInfo && this.datInfo.userSettings && this.datInfo.userSettings.isSaved + } + + get isMyProfile () { + var userSession = getUserSessionFor(this.browserWindow.webContents) + return this.datInfo && this.datInfo.url === userSession.url + } + get canGoBack () { return this.webContents.canGoBack() } @@ -682,7 +695,10 @@ class View { async fetchDatInfo (noEmit = false) { // clear existing state this.peers = 0 + this.isSystemDat = false + this.isFollowing = false this.numFollowers = 0 + this.confirmedAuthorTitle = undefined this.donateLinkHref = null if (!this.url.startsWith('dat://')) { @@ -700,11 +716,32 @@ class View { this.datInfo = null } if (this.datInfo) { + // mark as a system dat if it's the root drive or a user drive + let userUrls = beakerCore.users.listUrls() + if (beakerCore.filesystem.isRootUrl(this.datInfo.url) || userUrls.includes(this.datInfo.url)) { + this.isSystemDat = true + } + + // fetch social graph let userSession = getUserSessionFor(this.browserWindow.webContents) let userFollows = await beakerCore.uwg.follows.list({author: userSession.url}) let followAuthors = [userSession.url].concat(userFollows.map(f => f.topic.url)) let siteFollowers = await beakerCore.uwg.follows.list({topic: this.datInfo.url, author: followAuthors}) + this.isFollowing = siteFollowers.find(f => f.author.url === userSession.url) this.numFollowers = siteFollowers.length + + // determine the confirmed author + if (this.datInfo.author) { + try { + if (this.datInfo.author === userSession.url) { + this.confirmedAuthorTitle = (await beakerCore.users.get(userSession.url)).title + } else if (followAuthors.includes(this.datInfo.author)) { + this.confirmedAuthorTitle = userFollows.find(f => f.topic.url === this.datInfo.author).topic.title + } + } catch (e) { + console.error(e) + } + } } if (!noEmit) this.emitUpdateState() } diff --git a/app/new-shell-window/bg-process-rpc.js b/app/new-shell-window/bg-process-rpc.js index 2ad14eeacb..4110bf8a7c 100644 --- a/app/new-shell-window/bg-process-rpc.js +++ b/app/new-shell-window/bg-process-rpc.js @@ -1,12 +1,16 @@ import * as rpc from 'pauls-electron-rpc' import browserManifest from '@beaker/core/web-apis/manifests/internal/browser' import bookmarksManifest from '@beaker/core/web-apis/manifests/external/unwalled-garden-bookmarks' +import followsManifest from '@beaker/core/web-apis/manifests/external/unwalled-garden-follows' +import libraryManifest from '@beaker/core/web-apis/manifests/external/unwalled-garden-library' import watchlistManifest from '@beaker/core/web-apis/manifests/internal/watchlist' import viewsManifest from '../background-process/rpc-manifests/views' import datArchiveManifest from '@beaker/core/web-apis/manifests/external/dat-archive' export const beakerBrowser = rpc.importAPI('beaker-browser', browserManifest) export const bookmarks = rpc.importAPI('unwalled-garden-bookmarks', bookmarksManifest) +export const follows = rpc.importAPI('unwalled-garden-follows', followsManifest) +export const library = rpc.importAPI('unwalled-garden-library', libraryManifest) export const watchlist = rpc.importAPI('watchlist', watchlistManifest) export const views = rpc.importAPI('background-process-views', viewsManifest) export const datArchive = rpc.importAPI('dat-archive', datArchiveManifest) \ No newline at end of file diff --git a/app/new-shell-window/navbar.js b/app/new-shell-window/navbar.js index cad9c6b847..5263abd315 100644 --- a/app/new-shell-window/navbar.js +++ b/app/new-shell-window/navbar.js @@ -51,7 +51,7 @@ class ShellWindowNavbar extends LitElement { render () { return html` -
+
${this.backBtn} ${this.forwardBtn} ${this.reloadBtn} @@ -61,11 +61,14 @@ class ShellWindowNavbar extends LitElement { .activeTabIndex="${this.activeTabIndex}" url="${_get(this, 'activeTab.url', '')}" title="${_get(this, 'activeTab.title', '')}" - siteIcon="${_get(this, 'activeTab.siteIcon', '')}" siteTitle="${_get(this, 'activeTab.siteTitle', '')}" datDomain="${_get(this, 'activeTab.datDomain', '')}" - ?isOwner="${_get(this, 'activeTab.isOwner', false)}" + ?isOwner=${_get(this, 'activeTab.isOwner', false)} peers="${_get(this, 'activeTab.peers', 0)}" + ?canSave=${_get(this, 'activeTab.canSave', false)} + ?isSaved=${_get(this, 'activeTab.isSaved', false)} + ?canFollow=${_get(this, 'activeTab.canFollow', false)} + ?isFollowing=${_get(this, 'activeTab.isFollowing', false)} numFollowers="${_get(this, 'activeTab.numFollowers', 0)}" zoom="${_get(this, 'activeTab.zoom', '')}" .loadError=${_get(this, 'activeTab.loadError', null)} @@ -81,7 +84,7 @@ class ShellWindowNavbar extends LitElement { active-match="${_get(this, 'activeTab.currentInpageFindResults.activeMatchOrdinal', '0')}" num-matches="${_get(this, 'activeTab.currentInpageFindResults.matches', '0')}" > -
+
${this.watchlistBtn} ${this.usersMenuBtn} ${this.browserMenuBtn} diff --git a/app/new-shell-window/navbar/location.js b/app/new-shell-window/navbar/location.js index 3539f99e4f..ccc2abff79 100644 --- a/app/new-shell-window/navbar/location.js +++ b/app/new-shell-window/navbar/location.js @@ -18,10 +18,13 @@ class NavbarLocation extends LitElement { url: {type: String}, title: {type: String}, siteTitle: {type: String}, - siteIcon: {type: String}, datDomain: {type: String}, isOwner: {type: Boolean}, peers: {type: Number}, + canSave: {type: Boolean}, + isSaved: {type: Boolean}, + canFollow: {type: Boolean}, + isFollowing: {type: Boolean}, numFollowers: {type: Number}, zoom: {type: Number}, loadError: {type: Object}, @@ -40,10 +43,13 @@ class NavbarLocation extends LitElement { this.url = '' this.title = '' this.siteTitle = '' - this.siteIcon = '' this.datDomain = '' this.isOwner = false this.peers = 0 + this.canSave = false + this.isSaved = false + this.canFollow = false + this.isFollowing = false this.numFollowers = 0 this.zoom = 0 this.loadError = null @@ -86,7 +92,6 @@ class NavbarLocation extends LitElement { -
${this.renderLocation()} ${this.renderZoom()} ${this.renderLiveReloadingBtn()} ${this.renderAvailableAlternativeBtn()} ${this.renderDonateBtn()} - ${this.isDat ? html` -
- - -
- ` : ''} + ${this.renderFollowBtn()} + ${this.renderSaveBtn()} ${this.renderBookmarkBtn()} ` } @@ -163,14 +159,14 @@ class NavbarLocation extends LitElement { } } var cls = 'protocol' - if (['beaker:'].includes(protocol)) cls += ' protocol-secure' - if (['https:'].includes(protocol) && !this.loadError) cls += ' protocol-secure' + // if (['beaker:'].includes(protocol)) cls += ' protocol-secure' + // if (['https:'].includes(protocol) && !this.loadError) cls += ' protocol-secure' if (['https:'].includes(protocol) && this.loadError && this.loadError.isInsecureResponse) cls += ' protocol-insecure' - if (['dat:'].includes(protocol)) cls += ' protocol-secure' - if (['beaker:'].includes(protocol)) cls += ' protocol-secure' + // if (['dat:'].includes(protocol)) cls += ' protocol-secure' + // if (['beaker:'].includes(protocol)) cls += ' protocol-secure' return html`
- ${pathname}${search}${hash} + ${protocol.slice(0, -1)}://${host}${pathname}${search}${hash}
` } catch (e) { @@ -251,6 +247,26 @@ class NavbarLocation extends LitElement { ` } + renderFollowBtn () { + if (!this.canFollow) return '' + return html` + + ` + } + + renderSaveBtn () { + if (!this.canSave) return '' + return html` + + ` + } + renderBookmarkBtn () { const cls = classMap({ far: !this.isBookmarked, @@ -317,8 +333,22 @@ class NavbarLocation extends LitElement { e.currentTarget.blur() } - onClickFollow (e) { - // TODO + async onClickFollow (e) { + if (this.isFollowing) { + await bg.follows.remove(this.url) + } else { + await bg.follows.add(this.url) + } + bg.views.reload('active') + } + + async onClickSave (e) { + if (this.isSaved) { + await bg.library.configure(this.url, {isSaved: false}) + } else { + await bg.library.configure(this.url, {isSaved: true}) + } + bg.views.reload('active') } onClickZoom (e) { @@ -378,13 +408,11 @@ NavbarLocation.styles = [buttonResetCSS, css` border: 1px solid var(--color-border-input); border-radius: 4px; padding-right: 2px; + user-select: none; } -hr { - width: 0; - margin: 5px; - border: 0; - border-left: 1px solid var(--color-border-input); +shell-window-navbar-site-info { + margin-right: 5px; } button { @@ -400,6 +428,11 @@ button.text { font-size: 11px; } +button.text.highlight { + color: #157bcc; + font-weight: 500; +} + button .fa, button .far, button .fas { diff --git a/app/new-shell-window/navbar/site-info.js b/app/new-shell-window/navbar/site-info.js index 8b4282ac02..f4e9d6e2b8 100644 --- a/app/new-shell-window/navbar/site-info.js +++ b/app/new-shell-window/navbar/site-info.js @@ -10,7 +10,6 @@ class NavbarSiteInfo extends LitElement { static get properties () { return { url: {type: String}, - siteIcon: {type: String}, siteTitle: {type: String}, datDomain: {type: String}, isOwner: {type: Boolean}, @@ -23,7 +22,6 @@ class NavbarSiteInfo extends LitElement { constructor () { super() this.url = '' - this.siteIcon = '' this.siteTitle = '' this.datDomain = '' this.isOwner = false @@ -53,33 +51,39 @@ class NavbarSiteInfo extends LitElement { render () { const scheme = this.scheme + var certified = false + var insecure = false var innerHTML if (scheme) { const isHttps = scheme === 'https:' const isInsecureResponse = _get(this, 'loadError.isInsecureResponse') if ((isHttps && !isInsecureResponse) || scheme === 'beaker:') { + certified = true innerHTML = html` - ${scheme !== 'beaker:' ? html`` : ''} + ${this.siteTitle} ` } else if (scheme === 'http:') { + insecure = true innerHTML = html` ${this.siteTitle} ` } else if (isHttps && isInsecureResponse) { + insecure = true innerHTML = html` ${this.siteTitle} ` } else if (scheme === 'dat:') { + if (this.isOwner) { + certified = true + } innerHTML = html` - ${this.isOwner ? html`My Site` : ''} - - - ${this.siteAuthor ? html`${this.siteAuthor} ` : ''} - ${this.siteTitle} - + ${this.isOwner ? html` + + ` : ''} + ${this.siteTitle} ` } } @@ -90,8 +94,9 @@ class NavbarSiteInfo extends LitElement { return html` - ` } @@ -117,14 +122,31 @@ NavbarSiteInfo.styles = [buttonResetCSS, css` } button { - border-radius: 0; + border-radius: 16px; height: 26px; line-height: 26px; - padding: 0 4px 0 10px; + padding: 0 10px; + background: var(--bg-cert-default); +} + +button:not(:disabled):hover { + background: var(--bg-cert-default--hover); +} + +button.certified { + background: var(--bg-cert-certified); +} + +button.certified:hover { + background: var(--bg-cert-certified--hover); +} + +button.insecure { + background: var(--bg-cert-insecure); } -button:hover { - background: #eee; +button.insecure:hover { + background: var(--bg-cert-insecure--hover); } button.hidden { @@ -144,7 +166,7 @@ button.hidden { } .fa-caret-down { - color: #adadad; + color: rgba(0,0,0,.2); margin-left: 2px; } @@ -155,12 +177,8 @@ button.hidden { font-weight: 500; } -.label.darkbg { - background: rgba(0,0,0,.08); - padding: 2px 4px; - border-radius: 3px; - color: #444; - font-size: 10px; +.certified { + color: var(--color-certified); } .secure { diff --git a/app/new-shell-window/tabs.js b/app/new-shell-window/tabs.js index cce077e7c9..72706cabca 100644 --- a/app/new-shell-window/tabs.js +++ b/app/new-shell-window/tabs.js @@ -240,7 +240,6 @@ ${spinnerCSS} position: relative; padding: 0 18px 0 0px; height: 34px; - border-bottom: 1px solid var(--color-border); } .shell.win32 { @@ -258,7 +257,6 @@ ${spinnerCSS} position: relative; top: 0px; height: 34px; - border-left: 1px solid var(--color-border); } .tabs * { @@ -271,11 +269,15 @@ ${spinnerCSS} .tab { display: inline-block; position: relative; - top: 0px; - height: 34px; + top: 3px; + height: 31px; width: 235px; + margin-right: 2px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; min-width: 0; /* HACK: https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout */ - border: 1px solid transparent; + background: var(--bg-background); + transition: background 0.3s; } .tab.pinned { @@ -288,7 +290,7 @@ ${spinnerCSS} text-align: center; position: absolute; left: 10px; - top: 9px; + top: 7px; z-index: 3; } @@ -313,23 +315,17 @@ ${spinnerCSS} color: var(--color-tab); font-size: 11.5px; letter-spacing: 0.2px; - padding: 11px 11px 9px 30px; + padding: 9px 11px 9px 30px; height: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - border-left: 1px solid var(--color-border); } .tab.no-favicon .tab-title { padding-left: 11px; } -.tab.current .tab-title, -.tab.current + .tab .tab-title { - border-left-color: transparent; -} - .fa-volume-up, .fa-volume-mute { position: absolute; @@ -351,7 +347,7 @@ ${spinnerCSS} } .tab-close { - display: none; + opacity: 0; position: absolute; right: 8px; top: 8px; @@ -361,6 +357,8 @@ ${spinnerCSS} border-radius: 2px; text-align: center; color: var(--color-tab-close); + background: var(--bg-background); + transition: background 0.3s, opacity 0.3s; } .tab-close:before { @@ -370,6 +368,7 @@ ${spinnerCSS} font-weight: 200; opacity: 0; line-height: .71; + transition: opacity 0.3s; } .tab-close:hover:before, @@ -382,59 +381,14 @@ ${spinnerCSS} background: var(--bg-tab-close--hover); } -.tab.tab-add-btn { - top: 0; - width: 40px; -} - -.tab-add-btn .plus { - position: absolute; - top: 0; - display: block; - font-size: 22px; - font-weight: 300; - color: var(--color-tab-add); - margin: 4px 7px; - width: 26px; - height: 25px; - text-align: center; - line-height: 100%; - border-radius: 2px; -} - -.tab.tab-add-btn:hover { - background: inherit; -} - -.tab-add-btn:hover .tab-close:before { - opacity: 1; -} - -.tab-add-btn:hover .plus { - background: var(--bg-tab-add--hover); - color: var(--color-tab-add--hover); -} - -.tab:not(.current):hover .tab-title, +.tab:not(.current):hover, .tab:not(.current):hover .fa-volume-up, .tab:not(.current):hover .fa-volume-mute { background: var(--bg-tab--hover); } -/* add a gradient effect */ -.tab:not(.current):hover .tab-title:after { - content: ''; - display: block; - position: absolute; - right: 0; - top: 0; - height: 27px; - width: 60px; - background: linear-gradient(to right, #d2d2d200, #d2d2d2ff); -} - .tab:hover .tab-close { - display: block; + opacity: 1; background: var(--bg-tab--hover); } @@ -451,51 +405,62 @@ ${spinnerCSS} } .tab.current { - position: relative; background: var(--bg-tab--current); - border: 1px solid var(--color-border); - border-top: 0; - border-bottom: 0; - top: 1px; } -.tab.current:before { +.tab.current:before, +.tab.current:after { content: ''; position: absolute; - top: -1px; - left: -1px; - right: -1px; - height: 2px; - background: #0f8aea; + z-index: 1; + bottom: 0; + height: 10px; + width: 10px; + background: #fff; } -.tab.current .tab-favicon { - top: 8.5px; +.tab.current:before { + left: -10px; + -webkit-mask-image: radial-gradient(circle 10px at 0 0, transparent 0, transparent 10px, black 11px); } -.tab.current .tab-title:after { - /* adjust color */ - background: linear-gradient(to right, rgba(247,247,247,0), rgb(247, 247, 247)); +.tab.current:after { + right: -10px; + -webkit-mask-image: radial-gradient(circle 10px at 10px 0, transparent 0, transparent 10px, black 11px); } .tab.current .tab-close { background: var(--bg-tab--current); } -.tab.drag-hover .tab-title { - background: #bbb; +.tab.drag-hover { + background: var(--bg-tab--drag-over); +} + +.tab.tab-add-btn { + width: 40px; } -.tab.current.drag-hover { - border-color: #888; +.tab-add-btn .plus { + position: absolute; + top: 0; + display: block; + font-size: 22px; + font-weight: 300; + color: var(--color-tab-add); + margin: 3px 7px; + width: 26px; + height: 25px; + text-align: center; + line-height: 100%; } -.tab.current.drag-hover .tab-title { - background: #eee; +.tab-add-btn:hover .tab-close:before { + opacity: 1; } -.tab.current + .unused-space { - border-left-color: transparent; +.tab-add-btn:hover .plus { + color: var(--color-tab-add--hover); } /* make room for traffic lights */ diff --git a/app/shell-window.html b/app/shell-window.html index b60b582b49..732e9f5702 100644 --- a/app/shell-window.html +++ b/app/shell-window.html @@ -7,26 +7,34 @@ + + ` + } + }) +} \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/comments/composer.js b/app/userland/app-stdlib/js/com/comments/composer.js new file mode 100644 index 0000000000..80802a4695 --- /dev/null +++ b/app/userland/app-stdlib/js/com/comments/composer.js @@ -0,0 +1,114 @@ +import {LitElement, html} from '../../../vendor/lit-element/lit-element.js' +import commentComposerCSS from '../../../css/com/comments/composer.css.js' +import { emit } from '../../dom.js' + +export class CommentComposer extends LitElement { + static get properties () { + return { + topic: {type: String}, + replyTo: {type: String, attribute: 'reply-to'}, + isFocused: {type: Boolean}, + alwaysActive: {type: Boolean}, + draftText: {type: String}, + placeholder: {type: String} + } + } + + constructor () { + super() + this.topic = '' + this.replyTo = '' + this.isFocused = false + this.alwaysActive = false + this.draftText = '' + this.placeholder = 'Write a new comment' + } + + _submit () { + if (!this.draftText) return + var detail = { + topic: this.topic, + replyTo: this.replyTo || undefined, + body: this.draftText + } + emit(this, 'submit-comment', {bubbles: true, detail}) + this.draftText = '' + } + + focus () { + this.shadowRoot.querySelector('textarea').focus() + } + + // rendering + // = + + render () { + if (this.alwaysActive || this.isFocused || this.draftText) { + return this.renderActive() + } + return this.renderInactive() + } + + renderInactive () { + return html` +
+ ${this.placeholder} +
+ ` + } + + renderActive () { + return html` + +
+ +
+ ` + } + + // events + // = + + async onClickPlaceholder () { + this.isFocused = true + + // focus after update + await this.updateComplete + this.shadowRoot.querySelector('textarea').focus() + } + + onKeydownTextarea (e) { + // check for cmd/ctrl+enter + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + e.currentTarget.value = '' + e.currentTarget.blur() + return this._submit() + } + this.onChangeTextarea(e) + } + + onChangeTextarea (e) { + this.draftText = e.currentTarget.value + } + + onBlurTextarea () { + this.isFocused = false + } + + onClickPost () { + this._submit() + } +} +CommentComposer.styles = commentComposerCSS + +customElements.define('beaker-comment-composer', CommentComposer) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/comments/thread.js b/app/userland/app-stdlib/js/com/comments/thread.js new file mode 100644 index 0000000000..b897d7c280 --- /dev/null +++ b/app/userland/app-stdlib/js/com/comments/thread.js @@ -0,0 +1,143 @@ +import { LitElement, html } from '../../../vendor/lit-element/lit-element.js' +import { repeat } from '../../../vendor/lit-element/lit-html/directives/repeat.js' +import commentsThreadCSS from '../../../css/com/comments/thread.css.js' +import { timeDifference } from '../../time.js' +import { writeToClipboard } from '../../clipboard.js' +import { emit } from '../../dom.js' +import * as contextMenu from '../context-menu.js' +import * as toast from '../toast.js' +import './composer.js' +import '../reactions/reactions.js' + +export class CommentsThread extends LitElement { + static get properties () { + return { + comments: {type: Array}, + topicUrl: {type: String, attribute: 'topic-url'}, + userUrl: {type: String, attribute: 'user-url'}, + activeReplies: {type: Object}, + composerPlaceholder: {type: String, attribute: 'composer-placeholder'} + } + } + + constructor () { + super() + this.comments = null + this.topicUrl = '' + this.userUrl = '' + this.activeReplies = {} + this.composerPlaceholder = undefined + } + + render () { + return html` + + + ${this.renderComments(this.comments)} + ` + } + + renderComments (comments) { + if (!comments.length) return '' + return html` +
+ ${repeat(comments, c => c.url, c => this.renderComment(c))} +
+ ` + } + + renderComment (comment) { + return html` +
+ +
${comment.body}
+ + ${this.activeReplies[comment.url] ? html` + this.onSubmitComment(e, comment.url)} + > + ` : ''} + ${comment.replies && comment.replies.length ? this.renderComments(comment.replies) : ''} +
+ ` + } + + // events + // = + + async onClickToggleReply (e, url) { + this.activeReplies[url] = !this.activeReplies[url] + await this.requestUpdate() + if (this.activeReplies[url]) { + this.shadowRoot.querySelector(`beaker-comment-composer[reply-to="${url}"]`).focus() + } + } + + onSubmitComment (e, url) { + this.activeReplies[url] = false + this.requestUpdate() + } + + onClickMenu (e, comment) { + e.preventDefault() + e.stopPropagation() + + var items = [ + {icon: 'far fa-fw fa-file-alt', label: 'View comment', click: () => window.open(comment.url) }, + {icon: 'fas fa-fw fa-link', label: 'Copy comment URL', click: () => { + writeToClipboard(comment.url) + toast.create('Copied to your clipboard') + }} + ] + + if (this.userUrl === comment.author.url) { + items.push('-') + items.push({icon: 'fas fa-fw fa-trash', label: 'Delete comment', click: () => this.onClickDelete(comment) }) + } + + var rect = e.currentTarget.getClientRects()[0] + contextMenu.create({ + x: rect.right + 4, + y: rect.bottom + 8, + right: true, + withTriangle: true, + roomy: true, + noBorders: true, + fontAwesomeCSSUrl: 'beaker://assets/font-awesome.css', + style: `padding: 4px 0`, + items + }) + } + + onClickDelete (comment) { + if (!confirm('Are you sure?')) return + emit(this, 'delete-comment', {bubbles: true, composed: true, detail: {comment}}) + } +} +CommentsThread.styles = commentsThreadCSS + +customElements.define('beaker-comments-thread', CommentsThread) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/context-menu.js b/app/userland/app-stdlib/js/com/context-menu.js new file mode 100644 index 0000000000..28441c718b --- /dev/null +++ b/app/userland/app-stdlib/js/com/context-menu.js @@ -0,0 +1,216 @@ +import {LitElement, html, css} from '../../vendor/lit-element/lit-element.js' +import {classMap} from '../../vendor/lit-element/lit-html/directives/class-map.js' +import {ifDefined} from '../../vendor/lit-element/lit-html/directives/if-defined.js' +import {findParent} from '../dom.js' +import dropdownCSS from '../../css/com/dropdown.css.js' + +// globals +// = + +var resolve + +// exported api +// = + +// create a new context menu +// - returns a promise that will resolve to undefined when the menu goes away +// - example usage: +/* +create({ + // where to put the menu + x: e.clientX, + y: e.clientY, + + // align edge to right instead of left + right: true, + + // use triangle + withTriangle: true, + + // roomy style + roomy: true, + + // no borders on items + noBorders: false, + + // additional styles on dropdown-items + style: 'font-size: 14px', + + // parent element to append to + parent: document.body, + + // url to fontawesome css + fontAwesomeCSSUrl: '/vendor/beaker-app-stdlib/css/fontawesome.css', + + // menu items + items: [ + // icon from font-awesome + {icon: 'fa fa-link', label: 'Copy link', click: () => writeToClipboard('...')} + ] + + // instead of items, can give render() + render () { + return html` + + ` + } +} +*/ +export function create (opts) { + // destroy any existing + destroy() + + // extract attrs + var parent = opts.parent || document.body + + // render interface + parent.appendChild(new BeakerContextMenu(opts)) + document.addEventListener('keyup', onKeyUp) + document.addEventListener('click', onClickAnywhere) + + // return promise + return new Promise(_resolve => { + resolve = _resolve + }) +} + +export function destroy (value) { + const el = document.querySelector('beaker-context-menu') + if (el) { + el.parentNode.removeChild(el) + document.removeEventListener('keyup', onKeyUp) + document.removeEventListener('click', onClickAnywhere) + resolve(value) + } +} + +// global event handlers +// = + +function onKeyUp (e) { + e.preventDefault() + e.stopPropagation() + + if (e.keyCode === 27) { + destroy() + } +} + +function onClickAnywhere (e) { + if (!findParent(e.target, el => el.tagName === 'BEAKER-CONTEXT-MENU')) { + // click is outside the context-menu, destroy + destroy() + } +} + +// internal +// = + +export class BeakerContextMenu extends LitElement { + constructor ({x, y, right, withTriangle, roomy, noBorders, style, items, fontAwesomeCSSUrl, render}) { + super() + this.x = x + this.y = y + this.right = right || false + this.withTriangle = withTriangle || false + this.roomy = roomy || false + this.noBorders = noBorders || false + this.customStyle = style || undefined + this.items = items + this.fontAwesomeCSSUrl = fontAwesomeCSSUrl + this.customRender = render + } + + // calls the global destroy + // (this function exists so that custom renderers can destroy with this.destroy) + destroy () { + destroy() + } + + // rendering + // = + + render() { + const cls = classMap({ + 'dropdown-items': true, + right: this.right, + left: !this.right, + 'with-triangle': this.withTriangle, + roomy: this.roomy, + 'no-border': this.noBorders + }) + var style = '' + if (this.x) style += `left: ${this.x}px; ` + if (this.y) style += `top: ${this.y}px; ` + return html` + ${this.fontAwesomeCSSUrl ? html`` : ''} + ` + } +} + +BeakerContextMenu.styles = css` +${dropdownCSS} + +.context-menu { + position: fixed; + z-index: 10000; +} + +.dropdown-items { + width: auto; + white-space: nowrap; +} + +a.dropdown-item { + color: inherit; + text-decoration: none; +} + +.dropdown-item, +.dropdown-items.roomy .dropdown-item { + padding-right: 30px; /* add a little cushion to the right */ +} +` + +customElements.define('beaker-context-menu', BeakerContextMenu) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/files-explorer.js b/app/userland/app-stdlib/js/com/files-explorer.js new file mode 100644 index 0000000000..48bb13d241 --- /dev/null +++ b/app/userland/app-stdlib/js/com/files-explorer.js @@ -0,0 +1,498 @@ +import { LitElement, html } from '../../vendor/lit-element/lit-element.js' +import { repeat } from '../../vendor/lit-element/lit-html/directives/repeat.js' +import { format as formatBytes } from '../../vendor/bytes/index.js' +import * as contextMenu from './context-menu.js' +import * as toast from './toast.js' +import { joinPath } from '../strings.js' +import { emit } from '../dom.js' +import { writeToClipboard } from '../clipboard.js' +import sidebarFilesViewCSS from '../../css/com/files-explorer.css.js' + +class FilesExplorer extends LitElement { + static get properties () { + return { + url: {type: String, reflect: true}, + isRoot: {type: Boolean, attribute: 'is-root'}, + isLoading: {type: Boolean}, + readOnly: {type: Boolean}, + items: {type: Array} + } + } + + static get styles () { + return [sidebarFilesViewCSS] + } + + get isDat () { + return this.url && this.url.startsWith('dat:') + } + + get archive () { + return new DatArchive(this.url) + } + + get origin () { + let urlp = new URL(this.url) + return urlp.origin + } + + get viewedDatVersion () { + let urlp = new URL(this.url) + let parts = urlp.hostname.split('+') + if (parts.length === 2) return parts[1] + return 'latest' + } + + get pathname () { + let urlp = new URL(this.url) + return urlp.pathname + } + + constructor () { + super() + this.url = '' + this.isRoot = false + this.isLoading = true + this.readOnly = true + this.folderPath = '' + this.currentFolder = null + this.items = [] + this.load() + } + + attributeChangedCallback (name, oldval, newval) { + super.attributeChangedCallback(name, oldval, newval) + if (name === 'url') { + this.load() + } + } + + async load () { + this.isLoading = true + + var items = [] + if (this.isDat) { + let archive = this.archive + + let info = await archive.getInfo() + this.readOnly = !info.isOwner + + let st + let folderPath = this.pathname + while (!st && folderPath !== '/') { + try { st = await archive.stat(folderPath) } + catch (e) {/* ignore */} + if (!st || !st.isDirectory()) { + folderPath = (folderPath.split('/').slice(0, -1).filter(Boolean).join('/')) || '/' + } + } + this.folderPath = folderPath + + items = await archive.readdir(folderPath, {stat: true}) + items.sort((a, b) => { + if (a.stat.isDirectory() && !b.stat.isDirectory()) return -1 + if (!a.stat.isDirectory() && b.stat.isDirectory()) return 1 + return a.name.localeCompare(b.name) + }) + + this.currentFolder = await archive.stat(folderPath) + this.currentFolder.path = folderPath + this.currentFolder.name = folderPath.split('/').pop() || '/' + } + + this.items = items + this.isLoading = false + } + + // rendering + // = + + render () { + if (this.isLoading) { + return html` +
+
Loading...
+
+ ` + } + if (!this.isDat) { + return html` + +
+
This site doesn't support file listings
+
+ ` + } + const icon = item => { + if (item.stat.mount) return html`` + if (item.stat.isDirectory()) return html`` + return html`` + } + return html` + +
+ ${this.readOnly ? html ` +
This site is read-only
+ ` : html` + + + + `} + ${this.isRoot ? html` + + + This is your private filesystem + + ` : ''} +
+ +
+ ${this.folderPath !== '/' ? html` +
+ + .. +
+ ` : ''} + ${repeat(this.items, item => html` +
this.onClickItem(e, item)} @contextmenu=${e => this.onContextmenuItem(e, item)}> + ${icon(item)} + + ${item.name} + + + ${item.stat.size ? formatBytes(item.stat.size) : ''} + +
+ `)} +
+ ` + } + + // events + // = + + onContextmenuListing (e) { + e.preventDefault() + e.stopPropagation() + + contextMenu.create({ + x: e.clientX, + y: e.clientY, + fontAwesomeCSSUrl: '/vendor/beaker-app-stdlib/css/fontawesome.css', + noBorders: true, + roomy: true, + items: [ + { + icon: 'far fa-fw fa-folder', + label: 'New folder', + click: () => this.onClickNewFolder() + }, + { + icon: 'far fa-fw fa-file', + label: 'New file', + click: () => this.onClickNewFile() + }, + { + icon: 'fas fa-fw fa-upload', + label: 'Import files', + click: () => this.onClickImportFiles() + }, + { + icon: 'fas fa-fw fa-external-link-square-alt', + label: 'Mount', + click: () => this.onClickMount() + } + ] + }) + } + + onContextmenuItem (e, item) { + e.preventDefault() + e.stopPropagation() + + var url = joinPath(this.origin, this.folderPath || '', item.name || '') + var items = [] + items.push({ + icon: 'fas fa-fw fa-external-link-alt', + label: `Open in new tab`, + click: () => { + beaker.browser.openUrl(url, { + setActive: true, + isSidebarActive: true + }) + } + }) + if (item.stat.isFile()) { + items.push({ + icon: 'fas fa-fw fa-edit', + label: `Edit file`, + click: () => { + beaker.browser.gotoUrl(url) + beaker.browser.openSidebar('editor') + } + }) + } + items.push({ + icon: 'fas fa-fw fa-link', + label: `Copy URL`, + click () { + writeToClipboard(url) + toast.create('Copied to your clipboard') + } + }) + items.push({ + icon: 'fa fa-fw fa-i-cursor', + label: 'Rename', + click: async () => { + var newname = prompt(`Enter the new name for this ${item.stat.isDirectory() ? 'folder' : 'file'}`, item.name) + if (!newname) return + var oldpath = joinPath(this.folderPath, item.name) + var newpath = joinPath(this.folderPath, newname) + await this.archive.rename(oldpath, newpath) + if (oldpath === this.pathname) { + beaker.browser.gotoUrl(joinPath(this.origin, newpath)) + } else { + this.load() + } + } + }) + if (item.stat.mount) { + items = items.concat([ + '-', + { + icon: 'fas fa-fw fa-external-link-alt', + label: `Open mount in new tab`, + click: () => { + beaker.browser.openUrl(`dat://${item.stat.mount.key}`, { + setActive: true, + isSidebarActive: true + }) + } + }, + { + icon: 'fas fa-fw fa-link', + label: `Copy Mount URL`, + click () { + writeToClipboard(`dat://${item.stat.mount.key}`) + toast.create('Copied to your clipboard') + } + }, + { + icon: 'fas fa-fw fa-trash', + label: `Unmount`, + click: async () => { + if (confirm(`Are you sure you want to unmount ${item.name}?`)) { + let path = joinPath(this.folderPath, item.name) + await this.archive.unmount(path) + this.load() + } + } + } + ]) + } else { + items.push({ + icon: 'fa fa-fw fa-trash', + label: 'Delete', + click: async () => { + if (confirm(`Are you sure you want to delete ${item.name}?`)) { + let path = joinPath(this.folderPath, item.name) + if (item.stat.isDirectory()) { + await this.archive.rmdir(path, {recursive: true}) + } else { + await this.archive.unlink(path) + } + this.load() + } + } + }) + } + + contextMenu.create({ + x: e.clientX, + y: e.clientY, + fontAwesomeCSSUrl: '/vendor/beaker-app-stdlib/css/fontawesome.css', + noBorders: true, + roomy: true, + items + }) + } + + onClickAdditionalActions (e) { + e.preventDefault() + e.stopPropagation() + + var rect = e.currentTarget.getClientRects()[0] + contextMenu.create({ + x: rect.left - 3, + y: rect.bottom + 1, + fontAwesomeCSSUrl: '/vendor/beaker-app-stdlib/css/fontawesome.css', + noBorders: true, + roomy: true, + withTriangle: true, + items: [ + { + icon: 'fas fa-fw fa-external-link-square-alt', + label: 'Mount', + click: () => this.onClickMount() + }, + { + icon: 'fas fa-fw fa-upload', + label: 'Import files', + click: () => this.onClickImportFiles() + }, + { + icon: 'fas fa-fw fa-download', + label: 'Export files', + click: () => this.onClickExportFiles() + } + ] + }) + } + + onClickUpdog (e) { + var upPath = this.folderPath.split('/').filter(Boolean).slice(0, -1).join('/') + this.url = joinPath(this.origin, upPath) + } + + onClickItem (e, item) { + if (item.stat.isFile()) { + // open the file + let url = joinPath(this.origin, this.folderPath, item.name) + emit(this, 'open', {bubbles: true, composed: true, detail: {url}}) + } else { + // navigate in-UI to the folder + this.url = joinPath(this.origin, this.folderPath, item.name) + } + } + + onContextmenuCurrentFolder (e) { + e.preventDefault() + e.stopPropagation() + + var url = joinPath(this.origin, this.currentFolder.path) + var items = [] + items.push({ + icon: 'fas fa-fw fa-arrow-right', + label: `Go to this folder`, + click: () => { + beaker.browser.gotoUrl(url) + } + }) + items.push({ + icon: 'fas fa-fw fa-external-link-alt', + label: `Open in new tab`, + click: () => { + beaker.browser.openUrl(url, { + setActive: true, + isSidebarActive: true + }) + } + }) + items.push({ + icon: 'fas fa-fw fa-link', + label: `Copy URL`, + click () { + writeToClipboard(url) + toast.create('Copied to your clipboard') + } + }) + if (this.currentFolder.mount) { + items = items.concat([ + '-', + { + icon: 'fas fa-fw fa-external-link-alt', + label: `Open mount in new tab`, + click: () => { + beaker.browser.openUrl(`dat://${this.currentFolder.mount.key}`, { + setActive: true, + isSidebarActive: true + }) + } + }, + { + icon: 'fas fa-fw fa-link', + label: `Copy Mount URL`, + click () { + writeToClipboard(`dat://${this.currentFolder.mount.key}`) + toast.create('Copied to your clipboard') + } + } + ]) + } + + contextMenu.create({ + x: e.clientX, + y: e.clientY, + fontAwesomeCSSUrl: '/vendor/beaker-app-stdlib/css/fontawesome.css', + noBorders: true, + roomy: true, + items + }) + } + + async onClickNewFolder (e) { + if (this.readOnly) return + var name = prompt('Enter the new folder name') + if (name) { + let path = joinPath(this.folderPath, name) + await this.archive.mkdir(path) + this.load() + } + } + + async onClickNewFile (e) { + if (this.readOnly) return + var name = prompt('Enter the new file name') + if (name) { + let path = joinPath(this.folderPath, name) + await this.archive.writeFile(path, '') + this.load() + } + } + + async onClickImportFiles (e) { + if (this.readOnly) return + + let browserInfo = beaker.browser.getInfo() + var osCanImportFoldersAndFiles = browserInfo.platform === 'darwin' + + var files = await beaker.browser.showOpenDialog({ + title: 'Import files', + buttonLabel: 'Import', + properties: ['openFile', osCanImportFoldersAndFiles ? 'openDirectory' : false, 'multiSelections', 'createDirectory'].filter(Boolean) + }) + if (files) { + for (let src of files) { + await DatArchive.importFromFilesystem({ + src, + dst: joinPath(this.origin, this.folderPath), + ignore: ['dat.json'], + inplaceImport: false + }) + } + this.load() + } + } + + async onClickExportFiles (e) { + beaker.browser.downloadURL(`${this.origin}?download_as=zip`) + } + + async onClickMount (e) { + if (this.readOnly) return + + var url = await navigator.selectDatArchiveDialog() + if (!url) return + var name = await prompt('Enter the mount name') + if (!name) return + await this.archive.mount(name, url) + this.load() + } +} + +customElements.define('files-explorer', FilesExplorer) diff --git a/app/userland/app-stdlib/js/com/history-autocomplete.js b/app/userland/app-stdlib/js/com/history-autocomplete.js new file mode 100644 index 0000000000..325ab9ae47 --- /dev/null +++ b/app/userland/app-stdlib/js/com/history-autocomplete.js @@ -0,0 +1,175 @@ +import {LitElement, html} from '../../vendor/lit-element/lit-element.js' +import {repeat} from '../../vendor/lit-element/lit-html/directives/repeat.js' +import {classMap} from '../../vendor/lit-element/lit-html/directives/class-map.js' +import {toDomain, highlightSearchResult} from '../strings.js' +import historyAutocompleteCSS from '../../css/com/history-autocomplete.css.js' + +export class HistoryAutocomplete extends LitElement { + static get properties () { + return { + fontawesomeSrc: {type: String, attribute: 'fontawesome-src'}, + placeholder: {type: String}, + isFocused: {type: Boolean}, + query: {type: String}, + results: {type: Array}, + highlighted: {type: Number}, + includeVerbatim: {type: Boolean, attribute: 'include-verbatim'} + } + } + + constructor () { + super() + this.fontawesomeSrc = '' + this.placeholder = '' + this.isFocused = false + this.query = '' + this.results = null + this.highlighted = 0 + this.includeVerbatim = false + + this.$onClickDocument = this.onClickDocument.bind(this) + } + + async runQuery () { + var queryAtTimeOfRun = this.query + var res = this.query ? await beaker.history.search(this.query) : [] + console.log(res) + + if (queryAtTimeOfRun !== this.query) { + // user changed query while we were running, discard + console.log('Discarding results from outdated query') + return + } + + if (this.includeVerbatim) { + res = [{url: this.query, title: ''}].concat(res) + } + + this.highlighted = 0 + this.results = res + } + + get value () { + return this.query + } + + // rendering + // = + + render () { + return html` +
+ + ${this.renderResults()} +
+ ` + } + + renderResults () { + if (!this.results || !this.isFocused || this.results.length === 0) { + return '' + } + return html` +
+ ${repeat(this.results, (res, i) => this.renderResult(res, i))} +
+ ` + } + + renderResult (res, i) { + const cls = classMap({ + 'autocomplete-result': true, + 'search-result': true, + active: i === this.highlighted + }) + return html` + + + ${res.title} + ${res.url} + + ` + } + + // events + // = + + select (url, title) { + this.shadowRoot.querySelector('input').value = this.query = url + this.unfocus() + this.dispatchEvent(new CustomEvent('selection-changed', {detail: {title}})) + } + + unfocus () { + this.isFocused = false + + var input = this.shadowRoot.querySelector('input') + if (input.matches(':focus')) { + input.blur() + } + + document.removeEventListener('click', this.$onClickDocument) + } + + onClickResult (e) { + e.preventDefault() + this.select(e.currentTarget.getAttribute('href'), e.currentTarget.getAttribute('title')) + } + + onKeydownInput (e) { + if (e.key === 'Enter') { + e.preventDefault() + e.stopPropagation() + + let res = this.results[this.highlighted] + if (res) { + this.select(res.url, res.title) + } + return + } + if (e.key === 'Escape') { + return this.unfocus() + } + if (e.key === 'ArrowUp') { + e.preventDefault() + this.highlighted = Math.max(this.highlighted - 1, 0) + } + if (e.key === 'ArrowDown') { + e.preventDefault() + this.highlighted = Math.min(this.highlighted + 1, this.results.length) + } + } + + onKeyupInput (e) { + if (this.query !== e.currentTarget.value) { + this.query = e.currentTarget.value + this.runQuery() + } + } + + onFocusInput (e) { + this.isFocused = true + document.addEventListener('click', this.$onClickDocument) + } + + onClickDocument (e) { + // is the click inside us? + for (let el of e.path) { + if (el === this) return + } + // no, unfocus + this.unfocus() + } +} +HistoryAutocomplete.styles = historyAutocompleteCSS + +customElements.define('beaker-history-autocomplete', HistoryAutocomplete) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/hoverable.js b/app/userland/app-stdlib/js/com/hoverable.js new file mode 100644 index 0000000000..6cae0469a9 --- /dev/null +++ b/app/userland/app-stdlib/js/com/hoverable.js @@ -0,0 +1,50 @@ +import {LitElement, html} from '../../vendor/lit-element/lit-element.js' + +/* +Usage: + + + + + +*/ + +export class Hoverable extends LitElement { + static get properties () { + return { + isHovered: {type: Boolean} + } + } + + constructor () { + super() + this.isHovered = false + } + + render () { + if (this.isHovered) { + return html`` + } + return html`` + } + + onMouseenter () { + this.isHovered = true + + // HACK + // sometimes, if the mouse cursor leaves too quickly, 'mouseleave' doesn't get fired + // after a few ms, double check that it's still hovered + // -prf + setTimeout(() => { + if (!this.querySelector(':hover')) { + this.isHovered = false + } + }, 50) + } + + onMouseleave () { + this.isHovered = false + } +} + +customElements.define('beaker-hoverable', Hoverable) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/img-fallbacks.js b/app/userland/app-stdlib/js/com/img-fallbacks.js new file mode 100644 index 0000000000..5033d5e367 --- /dev/null +++ b/app/userland/app-stdlib/js/com/img-fallbacks.js @@ -0,0 +1,39 @@ +import {LitElement, html} from '../../vendor/lit-element/lit-element.js' + +/* +Usage: + + + + + + +*/ + +export class ImgFallbacks extends LitElement { + static get properties () { + return { + currentImage: {type: Number} + } + } + + constructor () { + super() + this.currentImage = 1 + } + + render () { + return html`` + } + + onSlotChange (e) { + var img = this.shadowRoot.querySelector('slot').assignedElements()[0] + if (img) img.addEventListener('error', this.onError.bind(this)) + } + + onError (e) { + this.currentImage = this.currentImage + 1 + } +} + +customElements.define('beaker-img-fallbacks', ImgFallbacks) diff --git a/app/userland/app-stdlib/js/com/popups/add-pinned-bookmark.js b/app/userland/app-stdlib/js/com/popups/add-pinned-bookmark.js new file mode 100644 index 0000000000..c10b139b4e --- /dev/null +++ b/app/userland/app-stdlib/js/com/popups/add-pinned-bookmark.js @@ -0,0 +1,258 @@ +/* globals beaker */ +import { html, css } from '../../../vendor/lit-element/lit-element.js' +import { BasePopup } from './base.js' +import popupsCSS from '../../../css/com/popups.css.js' +import { writeToClipboard } from '../../clipboard.js' +import * as contextMenu from '../context-menu.js' +import * as toast from '../toast.js' +import { toNiceUrl } from '../../strings.js' + +// exported api +// = + +export class AddPinnedBookmarkPopup extends BasePopup { + static get properties () { + return { + suggestions: {type: Object} + } + } + + constructor () { + super() + this.user = null + this.suggestions = {} + this.query = '' + this.isURLFocused = false + + this.initialLoad() + } + + // management + // + + static async create () { + return BasePopup.create(AddPinnedBookmarkPopup) + } + + static destroy () { + return BasePopup.destroy('beaker-add-pinned-bookmark-popup') + } + + async initialLoad () { + this.user = await uwg.profiles.me() + await this.loadSuggestions() + } + + async loadSuggestions () { + this.suggestions = await beaker.crawler.listSuggestions(this.user.url, this.query) + console.log(this.query, this.suggestions) + } + + // rendering + // = + + renderTitle () { + return 'Pin to start page' + } + + renderBody () { + var hasResults = !this.query || (Object.values(this.suggestions).filter(arr => arr.length > 0).length > 0) + return html` +
+ delay(this.onChangeQuery.bind(this), e)} /> +
+
+ ${hasResults ? '' : html`
No results
`} + ${this.renderSuggestionGroup('bookmarks', 'My Bookmarks')} + ${this.renderSuggestionGroup('websites', 'My Websites')} + ${this.renderSuggestionGroup('people', 'People')} + ${this.renderSuggestionGroup('themes', 'My Themes')} + ${this.renderSuggestionGroup('history', 'My Browsing History')} +
+ ` + } + + renderSuggestionGroup (key, label, useThumb = false) { + var group = this.suggestions[key] + if (!group || !group.length) return '' + return html` +
+
${label}
+
${group.map(g => this.renderSuggestion(g, useThumb))}
+
` + } + + renderSuggestion (row) { + const title = row.title || 'Untitled' + return html` + + + + ${title} + ${toNiceUrl(row.url)} + + + ` + } + + firstUpdated () { + this.shadowRoot.querySelector('input').focus() + } + + // events + // = + + onFocusSearch () { + if (!this.isURLFocused) { + this.isURLFocused = true + } + } + + async onChangeQuery (e) { + this.query = this.shadowRoot.querySelector('input').value + this.loadSuggestions() + } + + async pin (url, title) { + if (!(await uwg.bookmarks.has(url))) { + await uwg.bookmarks.add({href: url, title: title, pinned: true, isPublic: false}) + } else { + await uwg.bookmarks.edit(url, {pinned: true}) + } + toast.create('Pinned to your start page') + } + + async onClick (e) { + e.preventDefault() + await this.pin(e.currentTarget.getAttribute('href'), e.currentTarget.getAttribute('title')) + this.dispatchEvent(new CustomEvent('resolve')) + } + + onContextMenu (e) { + e.preventDefault() + var url = e.currentTarget.getAttribute('href') + const items = [ + {icon: 'fa fa-external-link-alt', label: 'Open Link in New Tab', click: () => window.open(url)}, + {icon: 'fa fa-link', label: 'Copy Link Address', click: () => writeToClipboard(url)} + ] + contextMenu.create({x: e.clientX, y: e.clientY, items, fontAwesomeCSSUrl: 'beaker://assets/font-awesome.css'}) + } +} +AddPinnedBookmarkPopup.styles = [popupsCSS, css` +.popup-inner { + width: 1000px; +} + +.popup-inner .body { + padding: 0; +} + +.filter-control { + padding: 8px 10px; + background: rgb(250, 250, 250); + border-bottom: 1px solid rgb(238, 238, 238); +} + +.filter-control input { + height: 26px; + margin: 0; + width: 100%; +} + +.suggestions { + overflow-y: auto; + max-height: calc(100vh - 300px); + padding-top: 20px; + margin-top: 0 !important; +} + +.empty { + color: rgba(0, 0, 0, 0.5); + padding: 0 20px 20px; +} + +.group { + padding: 0 0 20px; +} + +.group-title { + border-bottom: 1px solid rgba(0, 0, 0, 0.25); + color: rgba(0, 0, 0, 0.85); + margin-bottom: 10px; + padding-bottom: 5px; + padding-left: 20px; + letter-spacing: -0.5px; +} + +.group-items { + display: grid; + grid-template-columns: repeat(3, 1fr); + padding-right: 20px; +} + +.suggestion { + display: flex; + align-items: center; + padding: 10px; + overflow: hidden; + user-select: none; +} + +.suggestion .thumb { + flex: 0 0 80px; + width: 80px; + height: 64px; + object-fit: scale-down; + background: #fff; + margin: 0 20px 0 10px; + border: 1px solid #aaa; + border-radius: 3px; +} + +.suggestion .details { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; +} + +.suggestion .title, +.suggestion .url { + display: block; + white-space: nowrap; + line-height: 1.7; + overflow: hidden; + text-overflow: ellipsis; +} + +.suggestion .title { + font-size: 14px; + font-weight: 500; +} + +.suggestion .url { + font-size: 12px; + color: var(--blue); +} + +.suggestion:hover { + background: #eee; +} +`] + +customElements.define('beaker-add-pinned-bookmark-popup', AddPinnedBookmarkPopup) + + +// helpers +// = + +function trunc (str, n) { + if (str && str.length > n) { + str = str.slice(0, n - 3) + '...' + } + return str +} + +function delay (cb, param) { + window.clearTimeout(cb) + setTimeout(cb, 150, param) +} diff --git a/app/userland/app-stdlib/js/com/popups/base.js b/app/userland/app-stdlib/js/com/popups/base.js new file mode 100644 index 0000000000..4c5adf3f19 --- /dev/null +++ b/app/userland/app-stdlib/js/com/popups/base.js @@ -0,0 +1,106 @@ +import {LitElement, html} from '../../../vendor/lit-element/lit-element.js' +import popupsCSS from '../../../css/com/popups.css.js' + +// exported api +// = + +export class BasePopup extends LitElement { + constructor () { + super() + + const onGlobalKeyUp = e => { + // listen for the escape key + if (e.keyCode === 27) { + this.onReject() + } + } + document.addEventListener('keyup', onGlobalKeyUp) + + // cleanup function called on cancel + this.cleanup = () => { + document.removeEventListener('keyup', onGlobalKeyUp) + } + } + + get shouldCloseOnOuterClick () { + return true + } + + // management + // + + static async coreCreate (parentEl, Class, ...args) { + var popupEl = new Class(...args) + parentEl.appendChild(popupEl) + + const cleanup = () => { + popupEl.cleanup() + popupEl.remove() + } + + // return a promise that resolves with resolve/reject events + return new Promise((resolve, reject) => { + popupEl.addEventListener('resolve', e => { + resolve(e.detail) + cleanup() + }) + + popupEl.addEventListener('reject', e => { + reject() + cleanup() + }) + }) + } + + static async create (Class, ...args) { + return BasePopup.coreCreate(document.body, Class, ...args) + } + + static destroy (tagName) { + var popup = document.querySelector(tagName) + if (popup) popup.onReject() + } + + // rendering + // = + + render () { + return html` + + ` + } + + renderTitle () { + // should be overridden by subclasses + } + + renderBody () { + // should be overridden by subclasses + } + + // events + // = + + onClickWrapper (e) { + if (e.target.classList.contains('popup-wrapper') && this.shouldCloseOnOuterClick) { + this.onReject() + } + } + + onReject (e) { + if (e) e.preventDefault() + this.dispatchEvent(new CustomEvent('reject')) + } +} + +BasePopup.styles = [popupsCSS] \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/popups/edit-cover-photo.js b/app/userland/app-stdlib/js/com/popups/edit-cover-photo.js new file mode 100644 index 0000000000..4924d14e51 --- /dev/null +++ b/app/userland/app-stdlib/js/com/popups/edit-cover-photo.js @@ -0,0 +1,152 @@ +import { html, css } from '../../../vendor/lit-element/lit-element.js' +import { BasePopup } from './base.js' +import popupsCSS from '../../../css/com/popups.css.js' + +const CANVAS_WIDTH = 600 +const CANVAS_HEIGHT = 200 + +// exported api +// = + +export class BeakerEditCoverPhoto extends BasePopup { + static get properties () { + return { + currentImgUrl: {type: String} + } + } + + constructor (siteUrl, existingCoverPath) { + super() + this.siteUrl = siteUrl + this.loadedImg = null + this.currentImgUrl = '' + if (existingCoverPath) { + this.currentImgUrl = `${siteUrl}${existingCoverPath}` + } + } + + // management + // + + static async create (siteUrl, existingCoverPath) { + return BasePopup.create(BeakerEditCoverPhoto, siteUrl, existingCoverPath) + } + + static async runFlow (profiles) { + var profile = await profiles.me() + var archive = new DatArchive(profile.url) + + // find the existing cover + var existingCoverPath = null + const test = async (path) => { + if (existingCoverPath) return + var res = await archive.stat(path).catch(e => {}) + if (res) existingCoverPath = path + } + await test('/cover.jpg') + await test('/cover.jpeg') + await test('/cover.png') + + // run the modal + var img = await BeakerEditCoverPhoto.create(profile.url, existingCoverPath) + if (!img) return + + // replace any existing cover + await archive.unlink('/cover.jpg').catch(e => undefined) + await archive.unlink('/cover.jpeg').catch(e => undefined) + await archive.unlink('/cover.png').catch(e => undefined) + await archive.writeFile(`/cover.${img.ext}`, img.base64buf, 'base64') + } + + static destroy () { + return BasePopup.destroy('beaker-edit-cover-photo') + } + + // rendering + // = + + renderTitle () { + return `Update your cover photo` + } + + renderBody () { + return html` + +
+ +
+ + +
+
+ +
+ + +
+ + ` + } + + // events + // = + + async onClickThumb (e) { + e.preventDefault() + this.shadowRoot.querySelector('input[type="file"]').click() + } + + onChooseFile (e) { + var file = e.currentTarget.files[0] + if (!file) return + var fr = new FileReader() + fr.onload = () => { + var ext = file.name.split('.').pop() + this.currentImgUrl = fr.result + var base64buf = fr.result.split(',').pop() + this.loadedImg = {ext, base64buf} + } + fr.readAsDataURL(file) + + } + + onSubmit (e) { + e.preventDefault() + e.stopPropagation() + this.dispatchEvent(new CustomEvent('resolve', {detail: this.loadedImg})) + } +} +BeakerEditCoverPhoto.styles = [popupsCSS, css` +img { + display: block; + width: 600px; + height: 200px; + cursor: pointer; + margin-bottom: 10px; + object-fit: cover; +} + +img:hover { + opacity: 0.9; +} + +.controls { + display: flex; + flex-direction: column; + align-items: center; +} + +.popup-inner { + width: 630px; +} + +.popup-inner .actions { + justify-content: space-between; +} + +input[type="file"] { + display: none; +} +`] + +customElements.define('beaker-edit-cover-photo', BeakerEditCoverPhoto) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/popups/edit-profile.js b/app/userland/app-stdlib/js/com/popups/edit-profile.js new file mode 100644 index 0000000000..51a3c2b235 --- /dev/null +++ b/app/userland/app-stdlib/js/com/popups/edit-profile.js @@ -0,0 +1,108 @@ +import { html, css } from '../../../vendor/lit-element/lit-element.js' +import { BasePopup } from './base.js' +import popupsCSS from '../../../css/com/popups.css.js' + +// exported api +// = + +export class BeakerEditProfile extends BasePopup { + constructor (profile) { + super() + this.profile = profile + this.isCreate = !profile.title && !profile.description + } + + // management + // + + static async create (profile) { + return BasePopup.create(BeakerEditProfile, profile) + } + + static async runFlow (profiles) { + var profile = await profiles.me() + var newValues = await BeakerEditProfile.create(profile) + await (new DatArchive(profile.url)).configure(newValues) + return profiles.me() + } + + static destroy () { + return BasePopup.destroy('beaker-edit-profile') + } + + // rendering + // = + + renderTitle () { + return `${this.isCreate ? 'Create' : 'Edit'} your profile` + } + + renderBody () { + return html` +
+
+ + + + + + + +
+ +
+ + +
+
+ ` + } + + // events + // = + + onSubmit (e) { + e.preventDefault() + e.stopPropagation() + this.dispatchEvent(new CustomEvent('resolve', { + detail: { + title: e.target.title.value, + description: e.target.description.value + } + })) + } +} +BeakerEditProfile.styles = [popupsCSS, css` +img { + display: block; + margin: 10px auto; + border-radius: 50%; + height: 130px; + width: 130px; + object-fit: cover; +} + +.controls { + max-width: 460px; + margin: 20px auto 40px; +} + +.popup-inner { + width: 560px; +} + +.popup-inner textarea, +.popup-inner input { + margin-bottom: 20px; +} + +.popup-inner .actions { + justify-content: space-between; +} + +.hidden { + visibility: hidden; +} +`] + +customElements.define('beaker-edit-profile', BeakerEditProfile) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/popups/edit-thumb.js b/app/userland/app-stdlib/js/com/popups/edit-thumb.js new file mode 100644 index 0000000000..157dec845f --- /dev/null +++ b/app/userland/app-stdlib/js/com/popups/edit-thumb.js @@ -0,0 +1,171 @@ +import { html, css } from '../../../vendor/lit-element/lit-element.js' +import { BasePopup } from './base.js' +import popupsCSS from '../../../css/com/popups.css.js' + +const CANVAS_SIZE = 125 + +// exported api +// = + +export class BeakerEditThumb extends BasePopup { + constructor (siteUrl, existingThumbPath) { + super() + this.siteUrl = siteUrl + this.loadedImg = null + if (existingThumbPath) { + this.loadImg(`${siteUrl}${existingThumbPath}`) + } + } + + // management + // + + static async create (siteUrl, existingThumbPath) { + return BasePopup.create(BeakerEditThumb, siteUrl, existingThumbPath) + } + + static async runFlow (profiles) { + var profile = await profiles.me() + var archive = new DatArchive(profile.url) + + // find the existing thumb + var existingThumbPath = null + const test = async (path) => { + if (existingThumbPath) return + var res = await archive.stat(path).catch(e => {}) + if (res) existingThumbPath = path + } + await test('/thumb.jpg') + await test('/thumb.jpeg') + await test('/thumb.png') + + // run the modal + var img = await BeakerEditThumb.create(profile.url, existingThumbPath) + if (!img) return + + // replace any existing thumb + await archive.unlink('/thumb.jpg').catch(e => undefined) + await archive.unlink('/thumb.jpeg').catch(e => undefined) + await archive.unlink('/thumb.png').catch(e => undefined) + await archive.writeFile(`/thumb.${img.ext}`, img.base64buf, 'base64') + } + + static destroy () { + return BasePopup.destroy('beaker-edit-thumb') + } + + // rendering + // = + + renderTitle () { + return `Update your profile photo` + } + + renderBody () { + return html` +
+
+ +
+ + +
+
+ +
+ + +
+
+ ` + } + + // canvas handling + // = + + loadImg (url) { + this.zoom = 1 + this.img = document.createElement('img') + this.img.src = url + this.img.onload = () => { + var smallest = (this.img.width < this.img.height) ? this.img.width : this.img.height + this.zoom = CANVAS_SIZE / smallest + this.updateCanvas() + } + } + + updateCanvas () { + var canvas = this.shadowRoot.getElementById('thumb-canvas') + if (canvas) { + var ctx = canvas.getContext('2d') + ctx.globalCompositeOperation = 'source-over' + ctx.fillStyle = '#fff' + ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE) + ctx.save() + ctx.scale(this.zoom, this.zoom) + ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height) + ctx.restore() + } + } + + // events + // = + + async onClickThumb (e) { + e.preventDefault() + this.shadowRoot.querySelector('input[type="file"]').click() + } + + onChooseFile (e) { + var file = e.currentTarget.files[0] + if (!file) return + var fr = new FileReader() + fr.onload = () => { + var ext = file.name.split('.').pop() + this.loadImg(fr.result) + var base64buf = fr.result.split(',').pop() + this.loadedImg = {ext, base64buf} + } + fr.readAsDataURL(file) + } + + onSubmit (e) { + e.preventDefault() + e.stopPropagation() + this.dispatchEvent(new CustomEvent('resolve', {detail: this.loadedImg})) + } +} +BeakerEditThumb.styles = [popupsCSS, css` +canvas { + display: block; + margin: 0 30px 0 10px; + width: 125px; + height: 125px; + border-radius: 50%; + cursor: pointer; +} + +canvas:hover { + opacity: 0.5; +} + +.controls { + display: flex; + margin: 20px 20px 30px; + align-items: center; +} + +.popup-inner { + width: 360px; +} + +.popup-inner .actions { + justify-content: space-between; +} + +input[type="file"] { + display: none; +} +`] + +customElements.define('beaker-edit-thumb', BeakerEditThumb) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/popups/view-status.js b/app/userland/app-stdlib/js/com/popups/view-status.js new file mode 100644 index 0000000000..a29247635b --- /dev/null +++ b/app/userland/app-stdlib/js/com/popups/view-status.js @@ -0,0 +1,83 @@ +/* globals beaker */ +import { html, css } from '../../../vendor/lit-element/lit-element.js' +import { BasePopup } from './base.js' +import popupsCSS from '../../../css/com/popups.css.js' +import '../status/status.js' +import '../comments/thread.js' + +// exported api +// = + +export class ViewStatusPopup extends BasePopup { + static get styles () { + return [popupsCSS, css` + .popup-inner { + width: 560px; + overflow: visible; + } + + .popup-inner > .body { + padding: 4px 4px 8px; + } + + beaker-status { + margin: 0; + border: 0; + } + + beaker-comments-thread { + --border-color: #fff; + --body-font-size: 14px; + --composer-margin: 0 10px; + --composer-padding: 10px 16px; + --replies-left-margin: 4px; + margin-bottom: 10px; + } + `] + } + + constructor ({user, status}) { + super() + this.user = user + this.status = status + } + + // management + // + + static async create (parentEl, {user, status}) { + return BasePopup.coreCreate(parentEl, ViewStatusPopup, {user, status}) + } + + static destroy () { + return BasePopup.destroy('beaker-view-status-popup') + } + + // rendering + // = + + renderTitle () { + return 'Status' + } + + renderBody () { + return html` + + + ` + } + + // events + // = + +} + +customElements.define('beaker-view-status-popup', ViewStatusPopup) diff --git a/app/userland/app-stdlib/js/com/profile-info-card.js b/app/userland/app-stdlib/js/com/profile-info-card.js new file mode 100644 index 0000000000..c108c14ddf --- /dev/null +++ b/app/userland/app-stdlib/js/com/profile-info-card.js @@ -0,0 +1,92 @@ +import { LitElement, html } from '../../vendor/lit-element/lit-element.js' +import { toNiceDomain } from '../strings.js' +import { emit } from '../dom.js' +import buttonsCSS from '../../css/buttons.css.js' +import profileInfoCardCSS from '../../css/com/profile-info-card.css.js' +import './hoverable.js' + +export class ProfileInfoCard extends LitElement { + static get properties () { + return { + user: {type: Object}, + showControls: {type: Boolean, attribute: 'show-controls'}, + viewProfileBaseUrl: {type: String, attribute: 'view-profile-base-url'}, + fontawesomeSrc: {type: String, attribute: 'fontawesome-src'} + } + } + + constructor () { + super() + this.user = null + this.showControls = false + this.viewProfileBaseUrl = 'intent:unwalled.garden/view-profile?url=' + this.fontawesomeSrc = '' + } + + getViewUrl (user) { + return this.viewProfileBaseUrl ? `${this.viewProfileBaseUrl}${encodeURIComponent(user.url)}` : user.url + } + + render () { + if (!this.user) return html`
` + var viewProfileUrl = this.getViewUrl(this.user) + return html` + ${this.fontawesomeSrc ? html`` : ''} +
+
+ +
${this.user.description}
+ ${this.showControls ? this.renderControls() : ''} + ${this.showControls ? this.renderFollowers() : ''} + ` + } + + renderControls () { + if (this.user.isYou) { + return html` +
+ This is you + +
+ ` + } + return html` +
+ ${this.user.isFollowingYou ? html`Follows you` : html``} + ${this.user.isFollowed + ? html` + emit(this, 'unfollow', {detail: this.user})}> + + + ` + : html` + `} +
+ ` + } + + renderFollowers () { + const fs = this.user.followers + if (!fs || !fs.length) return '' + return html` +
+
Followed by
+
+ ${fs.map(f => html` + + `)} +
+
+ ` + } +} +ProfileInfoCard.styles = [buttonsCSS, profileInfoCardCSS] + +customElements.define('beaker-profile-info-card', ProfileInfoCard) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/reactions/picker.js b/app/userland/app-stdlib/js/com/reactions/picker.js new file mode 100644 index 0000000000..4161c1bfc6 --- /dev/null +++ b/app/userland/app-stdlib/js/com/reactions/picker.js @@ -0,0 +1,142 @@ +import { LitElement, html } from '../../../vendor/lit-element/lit-element.js' +import { classMap } from '../../../vendor/lit-element/lit-html/directives/class-map.js' +import pickerCSS from '../../../css/com/reactions/picker.css.js' +import * as EMOJIS from '../../../data/emoji-list.js' +import { render as renderEmoji, setSkinTone } from '../../emoji.js' +import { findParent } from '../../dom.js' + +const SKIN_TONE_EMOJIS = [`🏻`,`🏼`,`🏽`,`🏾`,`🏿`] + +export class ReactionPicker extends LitElement { + static get properties () { + return { + skinTone: {type: Number} + } + } + + constructor () { + super() + + this.skinTone = +localStorage.lastSkinTone || 0 + + const onGlobalKeyUp = e => { + // listen for the escape key + if (e.keyCode === 27) { + this.onReject() + } + } + const onGlobalClick = e => { + // listen for clicks outside the picker + if (findParent(e.target, e => e.tagName === 'BEAKER-REACTION-PICKER')) { + return + } + this.onReject() + } + document.addEventListener('keyup', onGlobalKeyUp) + document.addEventListener('click', onGlobalClick) + + // cleanup function called on cancel + this.cleanup = () => { + document.removeEventListener('keyup', onGlobalKeyUp) + document.removeEventListener('click', onGlobalClick) + } + } + + // management + // + + static async create ({left, top}) { + var el = new ReactionPicker() + document.body.appendChild(el) + + el.style.left = `${left}px` + el.style.top = `${top}px` + + const cleanup = () => { + el.cleanup() + el.remove() + } + + // return a promise that resolves with resolve/reject events + return new Promise((resolve, reject) => { + el.addEventListener('resolve', e => { + resolve(e.detail) + cleanup() + }) + + el.addEventListener('reject', e => { + reject() + cleanup() + }) + }) + } + + static destroy (tagName) { + var popup = document.querySelector(tagName) + if (popup) popup.onReject() + } + + // rendering + // = + + render () { + return html` +
+
+ Reactions +
+
+ ${this.renderSkinToneOption(0)} + ${this.renderSkinToneOption(1)} + ${this.renderSkinToneOption(2)} + ${this.renderSkinToneOption(3)} + ${this.renderSkinToneOption(4)} + ${this.renderSkinToneOption(5)} +
+
+
+
Frequently used
+
${EMOJIS.SUGGESTED.map(this.renderEmoji.bind(this))}
+ ${EMOJIS.GROUPS.map(group => html` +
${group.name}
+
${group.emojis.map(this.renderEmoji.bind(this))}
+ `)} +
+ ` + } + + renderSkinToneOption (n) { + return html` + this.onSelectSkinTone(e, n)}> + ${n === 0 ? html`` : SKIN_TONE_EMOJIS[n - 1]} + + ` + } + + renderEmoji (emoji) { + return html` this.onClickEmoji(e, emoji)}>${renderEmoji(emoji, this.skinTone)}` + } + + // events + // = + + onReject (e) { + if (e) e.preventDefault() + this.dispatchEvent(new CustomEvent('reject')) + } + + onClickEmoji (e, emoji) { + emoji = setSkinTone(emoji, this.skinTone) + this.dispatchEvent(new CustomEvent('resolve', {detail: emoji})) + } + + onSelectSkinTone (e, n) { + e.preventDefault() + e.stopPropagation() + this.skinTone = n + localStorage.lastSkinTone = n + } +} +ReactionPicker.styles = pickerCSS + +customElements.define('beaker-reaction-picker', ReactionPicker) diff --git a/app/userland/app-stdlib/js/com/reactions/reactions.js b/app/userland/app-stdlib/js/com/reactions/reactions.js new file mode 100644 index 0000000000..a337ff7cb5 --- /dev/null +++ b/app/userland/app-stdlib/js/com/reactions/reactions.js @@ -0,0 +1,134 @@ +import { LitElement, html } from '../../../vendor/lit-element/lit-element.js' +import { classMap } from '../../../vendor/lit-element/lit-html/directives/class-map.js' +import { ifDefined } from '../../../vendor/lit-element/lit-html/directives/if-defined.js' +import { ucfirst } from '../../strings.js' + +const DEFAULT_PHRASES = ['like', 'agree', 'haha'] + +export class Reactions extends LitElement { + static get properties () { + return { + reactions: {type: Object}, + userUrl: {type: String, attribute: 'user-url'}, + topic: {type: String} + } + } + + constructor () { + super() + this.reactions = [] + this.userUrl = '' + this.topic = '' + } + + createRenderRoot () { + // dont use the shadow dom + // this enables the post's hover state to hide/show the add button + return this + } + + get defaultReactions () { + var reactions = this.reactions || [] + return DEFAULT_PHRASES.map(phrase => reactions.find(r => r.phrase === phrase) || {phrase, authors: []}) + } + + get addedReactions () { + return this.reactions.filter(r => !DEFAULT_PHRASES.includes(r.phrase)) + } + + render () { + const renderReaction = r => { + var alreadySet = !!r.authors.find(a => a.url === this.userUrl) + var cls = classMap({reaction: true, pressed: alreadySet}) + return html` + this.emitChange(e, alreadySet, r.phrase)} + data-tooltip=${ifDefined(r.authors.length ? r.authors.map(a => a.title || 'Anonymous').join(', ') : undefined)} + > + ${ucfirst(r.phrase)} + ${r.authors.length > 0 ? html` + ${r.authors.length} + `: ''} + + ` + } + + var reactions = this.reactions.slice() + for (let i = 0; i < DEFAULT_PHRASES.length && reactions.length < 4; i++) { + if (!reactions.find(r => r.phrase === DEFAULT_PHRASES[i])) { + reactions.push({phrase: DEFAULT_PHRASES[i], authors: []}) + } + } + + return html` + ${reactions.map(renderReaction)} + + + + ` + } + + // events + // = + + emitChange (e, alreadySet, phrase) { + e.preventDefault() + e.stopPropagation() + if (alreadySet) this.emitRemove(phrase) + if (!alreadySet) this.emitAdd(phrase) + } + + emitAdd (phrase) { + this.dispatchEvent(new CustomEvent('add-reaction', {bubbles: true, composed: true, detail: {topic: this.topic, phrase}})) + + // optimistic update UI + var author = {url: this.userUrl, title: 'You'} + var reaction = this.reactions.find(r => r.phrase === phrase) + if (reaction) reaction.authors.push(author) + else this.reactions.push({phrase, authors: [author]}) + this.requestUpdate() + } + + emitRemove (phrase) { + this.dispatchEvent(new CustomEvent('delete-reaction', {bubbles: true, composed: true, detail: {topic: this.topic, phrase}})) + + // optimistic update UI + var reaction = this.reactions.find(r => r.phrase === phrase) + if (reaction) reaction.authors = reaction.authors.filter(author => author.url !== this.userUrl) + this.requestUpdate() + } + + async onClickOther (e) { + e.preventDefault() + e.stopPropagation() + + do { + var phrase = prompt('Enter a custom reaction (characters only)') + if (!phrase) break + if (phrase.length > 20) { + alert('Must be 20 characters or less') + continue + } + if (/^[a-z ]+$/i.test(phrase) === false) { + alert('Must only be characters (a-z)') + continue + } + + this.emitAdd(phrase.toLowerCase()) + break + } while (true) + } +} + +customElements.define('beaker-reactions', Reactions) + +// helpers +//- + +function offset (el) { + var rect = el.getBoundingClientRect(), + scrollLeft = window.pageXOffset || document.documentElement.scrollLeft, + scrollTop = window.pageYOffset || document.documentElement.scrollTop; + return { top: rect.top + scrollTop, left: rect.left + scrollLeft, right: rect.right + scrollLeft, bottom: rect.bottom + scrollTop } +} \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/status/bookmark.js b/app/userland/app-stdlib/js/com/status/bookmark.js new file mode 100644 index 0000000000..e72abbabee --- /dev/null +++ b/app/userland/app-stdlib/js/com/status/bookmark.js @@ -0,0 +1,55 @@ +import {LitElement, html} from '../../../vendor/lit-element/lit-element.js' +import feedBookmarkCSS from '../../../css/com/feed/bookmark.css.js' +import {timeDifference} from '../../time.js' +import { toNiceDomain } from '../../strings.js' +import '../reactions/reactions.js' + +export class FeedBookmark extends LitElement { + static get properties () { + return { + bookmark: {type: Object}, + userUrl: {type: String, attribute: 'user-url'}, + viewProfileBaseUrl: {type: String, attribute: 'view-profile-base-url'} + } + } + + constructor () { + super() + this.bookmark = null + this.userUrl = '' + this.viewProfileBaseUrl = 'intent:unwalled.garden/view-profile?url=' + } + + render () { + if (!this.bookmark) return + var isOwner = this.userUrl === this.bookmark.author.url + var viewProfileUrl = `${this.viewProfileBaseUrl}${encodeURIComponent(this.bookmark.author.url)}` + return html` + +
+
+
+
+ ${this.bookmark.title || 'Untitled'} + ${toNiceDomain(this.bookmark.href)} +
+ ${this.bookmark.description ? html`
${this.bookmark.description}
` : ''} +
+ +
+
+ ` + } +} +FeedBookmark.styles = feedBookmarkCSS + +customElements.define('beaker-feed-bookmark', FeedBookmark) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/status/composer.js b/app/userland/app-stdlib/js/com/status/composer.js new file mode 100644 index 0000000000..381ea0ec28 --- /dev/null +++ b/app/userland/app-stdlib/js/com/status/composer.js @@ -0,0 +1,99 @@ +import {LitElement, html} from '../../../vendor/lit-element/lit-element.js' +import composerCSS from '../../../css/com/status/composer.css.js' +import { on } from '../../dom.js' + +export class StatusComposer extends LitElement { + static get properties () { + return { + isFocused: {type: Boolean}, + draftText: {type: String} + } + } + + constructor () { + super() + this.isFocused = false + this.draftText = '' + on(document, 'focus-composer', () => this.onClickPlaceholder()) + } + + _submit () { + if (!this.draftText) return + this.dispatchEvent(new CustomEvent('submit', {detail: {body: this.draftText}})) + this.draftText = '' + } + + // rendering + // = + + render () { + if (this.isFocused || this.draftText) { + return this.renderActive() + } + return this.renderInactive() + } + + renderInactive () { + return html` + +
+ Compose a new status +
+ ` + } + + renderActive () { + return html` + +
+ +
+ ` + } + + // events + // = + + async onClickPlaceholder () { + this.isFocused = true + + // focus after update + await this.updateComplete + this.shadowRoot.querySelector('textarea').focus() + } + + onKeydownTextarea (e) { + // check for cmd/ctrl+enter + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + e.currentTarget.value = '' + e.currentTarget.blur() + return this._submit() + } + this.onChangeTextarea(e) + } + + onChangeTextarea (e) { + this.draftText = e.currentTarget.value + } + + onBlurTextarea () { + this.isFocused = false + } + + onClickPost () { + this._submit() + } +} +StatusComposer.styles = composerCSS + +customElements.define('beaker-status-composer', StatusComposer) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/status/feed.js b/app/userland/app-stdlib/js/com/status/feed.js new file mode 100644 index 0000000000..6e2d1cc6a3 --- /dev/null +++ b/app/userland/app-stdlib/js/com/status/feed.js @@ -0,0 +1,218 @@ +import { LitElement, html, css } from '../../../vendor/lit-element/lit-element.js' +import { repeat } from '../../../vendor/lit-element/lit-html/directives/repeat.js' +import * as toast from '../toast.js' +import feedCSS from '../../../css/com/status/feed.css.js' +import './status.js' +import './composer.js' +import { ViewStatusPopup } from '../popups/view-status.js' + +const LOAD_LIMIT = 50 + +export class StatusFeed extends LitElement { + static get properties () { + return { + user: {type: Object}, + author: {type: String}, + statuses: {type: Array} + } + } + + static get styles () { + return feedCSS + } + + get feedAuthors () { + if (!this.user) return [] + return [this.user.url].concat(this.followedUsers) + } + + constructor () { + super() + this.user = undefined + this.author = undefined + this.followedUsers = [] + this.statuses = [] + this.poppedUpStatus = undefined + } + + async load () { + this.followedUsers = (await uwg.follows.list({author: this.user.url})).map(({topic}) => topic.url) + var statuses = await uwg.statuses.list({ + author: this.author ? this.author : this.feedAuthors, + limit: LOAD_LIMIT, + reverse: true + }) + statuses = statuses.filter(status => status.body) + await this.loadFeedAnnotations(statuses) + this.statuses = statuses + console.log(this.statuses) + } + + async refreshFeed () { + var statuses = this.statuses.slice() + await this.loadFeedAnnotations(statuses) + this.statuses = [] // HACK - should find the right way to get litelement to rerender -prf + await this.requestUpdate() + this.statuses = statuses + } + + async loadFeedAnnotations (statuses) { + await Promise.all(statuses.map(async (status) => { + var [c, r] = await Promise.all([ + uwg.comments.list({topic: status.url, author: this.feedAuthors}), + uwg.reactions.tabulate(status.url, {author: this.feedAuthors}) + ]) + status.numComments = c.length + status.reactions = r + })) + } + + async loadStatusComments (status) { + status.comments = await uwg.comments.thread(status.url, {author: this.feedAuthors}) + await loadCommentReactions(this.feedAuthors, status.comments) + console.log('loaded', status.comments) + } + + async updatePopup () { + if (this.poppedUpStatus) { + await this.loadStatusComments(this.poppedUpStatus) + this.shadowRoot.querySelector('beaker-view-status-popup').requestUpdate() + } + } + + + render () { + return html` + +
+ ${!this.author ? html` + + ` : ''} + ${repeat(this.statuses, status => html` + + `)} + ${this.statuses.length === 0 + ? html` +
+
+
+ ${this.author + ? 'This user has not posted anything.' + : 'This is your feed. It will show statuses from sites you follow.'} +
+
+ ` : ''} +
+ + ` + } + + // events + // = + + async onExpandStatus (e) { + this.poppedUpStatus = e.detail.status + await this.loadStatusComments(this.poppedUpStatus) + try { + await ViewStatusPopup.create(this.shadowRoot.querySelector('.popup-container'), {user: this.user, status: this.poppedUpStatus}) + } catch (e) {/* ignore */} + this.poppedUpStatus = null + this.refreshFeed() + } + + async onAddReaction (e) { + await uwg.reactions.add(e.detail.topic, e.detail.phrase) + } + + async onDeleteReaction (e) { + await uwg.reactions.remove(e.detail.topic, e.detail.phrase) + } + + async onSubmitStatus (e) { + // add the new status + try { + await uwg.statuses.add({body: e.detail.body}) + } catch (e) { + alert('Something went wrong. Please let the Beaker team know! (An error is logged in the console.)') + console.error('Failed to add status') + console.error(e) + return + } + + // reload the feed to show the new status + this.load() + } + + async onDeleteStatus (e) { + let status = e.detail.status + + // delete the status + try { + await uwg.statuses.remove(status.url) + } catch (e) { + alert('Something went wrong. Please let the Beaker team know! (An error is logged in the console.)') + console.error('Failed to delete status') + console.error(e) + return + } + toast.create('Status deleted') + + // remove from the feed + this.statuses = this.statuses.filter(s => s.url !== status.url) + } + + async onSubmitComment (e) { + // add the new comment + try { + var {topic, replyTo, body} = e.detail + await uwg.comments.add(topic, {replyTo, body}) + } catch (e) { + alert('Something went wrong. Please let the Beaker team know! (An error is logged in the console.)') + console.error('Failed to add comment') + console.error(e) + return + } + this.updatePopup() + } + + async onDeleteComment (e) { + let comment = e.detail.comment + + // delete the comment + try { + await uwg.statuses.remove(comment.url) + } catch (e) { + alert('Something went wrong. Please let the Beaker team know! (An error is logged in the console.)') + console.error('Failed to delete comment') + console.error(e) + return + } + toast.create('Comment deleted') + this.updatePopup() + } +} + +customElements.define('beaker-status-feed', StatusFeed) + +async function loadCommentReactions (author, comments) { + await Promise.all(comments.map(async (comment) => { + comment.reactions = await uwg.reactions.tabulate(comment.url, {author}) + comment.reactions.sort((a, b) => b.authors.length - a.authors.length) + if (comment.replies) await loadCommentReactions(author, comment.replies) + })) +} \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/status/status.js b/app/userland/app-stdlib/js/com/status/status.js new file mode 100644 index 0000000000..4be8225503 --- /dev/null +++ b/app/userland/app-stdlib/js/com/status/status.js @@ -0,0 +1,138 @@ +import { LitElement, html } from '../../../vendor/lit-element/lit-element.js' +import { unsafeHTML } from '../../../vendor/lit-element/lit-html/directives/unsafe-html.js' +import statusCSS from '../../../css/com/status/status.css.js' +import { timeDifference } from '../../time.js' +import { findParent, emit } from '../../dom.js' +import { writeToClipboard } from '../../clipboard.js' +import { highlightSearchResult } from '../../strings.js' +import '../reactions/reactions.js' +import * as contextMenu from '../context-menu.js' +import * as toast from '../toast.js' + +const RENDER_LIMIT = 280 + +export class Status extends LitElement { + static get properties () { + return { + status: {type: Object}, + userUrl: {type: String, attribute: 'user-url'}, + highlightNonce: {type: String, attribute: 'highlight-nonce'}, + expanded: {type: Boolean}, + viewProfileBaseUrl: {type: String, attribute: 'view-profile-base-url'}, + viewRecordBaseUrl: {type: String, attribute: 'view-record-base-url'} + } + } + + constructor () { + super() + this.status = null + this.userUrl = '' + this.highlightNonce = undefined + this.expanded = false + this.viewProfileBaseUrl = '' + this.viewRecordBaseUrl = '' + } + + get isTooLong () { + return !this.expanded && this.status.body.length > RENDER_LIMIT + } + + render () { + if (!this.status || !this.status.body) return + var viewProfileUrl = this.viewProfileBaseUrl ? `${this.viewProfileBaseUrl}${encodeURIComponent(this.status.author.url)}` : this.status.author.url + var body = this.expanded ? this.status.body : this.status.body.slice(0, RENDER_LIMIT) + if (this.highlightNonce !== undefined) { + body = unsafeHTML(highlightSearchResult(body, this.highlightNonce)) + } + return html` + +
+
+ +
${body}${this.isTooLong ? '...' : ''}
+ ${this.isTooLong ? html`Read more` : ''} + ${''/* TODO
+
+ +
+
+
Paul Frazee
+
Website
+
The Beaker guy
+
*/} + +
+
+ ` + } + + // events + // = + + onTopClick (e) { + // make sure this wasn't a click on a link within the status + var aEl = findParent(e.target, el => el.tagName === 'A' || el === e.currentTarget) + if (aEl !== e.currentTarget && aEl.getAttribute('href') !== '#') { + return + } + e.preventDefault() + e.stopPropagation() + emit(this, 'expand', {bubbles: true, composed: true, detail: {status: this.status}}) + } + + onClickMenu (e) { + e.preventDefault() + e.stopPropagation() + + var items = [ + {icon: 'far fa-fw fa-file-alt', label: 'View status', click: () => window.open(this.status.url) }, + {icon: 'fas fa-fw fa-link', label: 'Copy status URL', click: () => { + writeToClipboard(this.status.url) + toast.create('Copied to your clipboard') + }} + ] + + if (this.userUrl === this.status.author.url) { + items.push('-') + items.push({icon: 'fas fa-fw fa-trash', label: 'Delete status', click: () => this.onClickDelete() }) + } + + var rect = e.currentTarget.getClientRects()[0] + contextMenu.create({ + x: rect.right + 4, + y: rect.bottom + 8, + right: true, + withTriangle: true, + roomy: true, + noBorders: true, + fontAwesomeCSSUrl: 'beaker://assets/font-awesome.css', + style: `padding: 4px 0`, + items + }) + } + + onClickDelete () { + if (!confirm('Are you sure?')) return + emit(this, 'delete', {bubbles: true, composed: true, detail: {status: this.status}}) + } +} +Status.styles = statusCSS + +customElements.define('beaker-status', Status) \ No newline at end of file diff --git a/app/userland/app-stdlib/js/com/table.js b/app/userland/app-stdlib/js/com/table.js new file mode 100644 index 0000000000..5f50913607 --- /dev/null +++ b/app/userland/app-stdlib/js/com/table.js @@ -0,0 +1,191 @@ +import {LitElement, html} from '../../vendor/lit-element/lit-element.js' +import {classMap} from '../../vendor/lit-element/lit-html/directives/class-map.js' +import {styleMap} from '../../vendor/lit-element/lit-html/directives/style-map.js' +import {repeat} from '../../vendor/lit-element/lit-html/directives/repeat.js' +import tableCSS from '../../css/com/table.css.js' + +/** + * @typedef {Object} ColumnDef + * @property {string} id + * @property {string=} label + * @property {number=} flex + * @property {number=} width + * @property {string=} renderer + */ + +export class Table extends LitElement { + static get properties() { + return { + rows: {type: Array} + } + } + + /** + * @type ColumnDef[] + */ + get columns () { + // this should be overridden by subclasses + return [ + {id: 'example', label: 'Example', flex: 1}, + {id: 'column2', label: 'Column2', width: 150} + ] + } + + get hasHeadingLabels () { + return !!this.columns.find(col => !!col.label) + } + + getRowKey (row) { + // this can be overridden by subclasses + return row + } + + getRowHref (row) { + // this can be overridden by subclasses + // if a string is returned, the row will become a link + return false + } + + isRowSelected (row) { + // this can be overridden by subclasses + return false + } + + sort () { + // this can be overridden by subclasses + } + + constructor (opts = {}) { + super() + this.rows = [] + this.sortColumn = this.columns[0] ? this.columns[0].id : '' + this.sortDirection = 'asc' + + if (opts.fontAwesomeCSSUrl) { + this.fontAwesomeCSSUrl = opts.fontAwesomeCSSUrl + } + } + + // rendering + // = + + render() { + return html` + + ${this.hasHeadingLabels + ? html` +
+ ${repeat(this.columns, col => this.renderHeadingColumn(col))} +
+ ` : ''} +
+ ${repeat(this.rows, row => this.getRowKey(row), row => this.renderRow(row))} + ${this.rows.length === 0 ? this.renderEmpty() : ''} +
+ ` + } + + getColumnClasses (column) { + return classMap({ + col: true, + [column.id]: true, + stretch: column.stretch + }) + } + + getColumnStyles (column) { + const styles = {} + if (column.width) { + styles.width = `${column.width}px` + } + if (column.flex) { + styles.flex = column.flex + } + return styleMap(styles) + } + + renderHeadingColumn (column) { + const cls = this.getColumnClasses(column) + const styles = this.getColumnStyles(column) + return html` +
+ this.onClickHeadingColumn(e, column)}>${column.label} + ${this.renderSortIcon(column)} +
+ ` + } + + renderRow (row) { + const cls = classMap({ + row: true, + selected: this.isRowSelected(row) + }) + const columns = repeat(this.columns, col => this.renderRowColumn(col, row)) + const href = this.getRowHref(row) + if (href) { + return html` this.onClickRow(e, row)} @contextmenu=${e => this.onContextmenuRow(e, row)}>${columns}` + } + return html` +
this.onClickRow(e, row)} + @dblclick=${e => this.onDblclickRow(e, row)} + @contextmenu=${e => this.onContextmenuRow(e, row)} + >${columns}
` + } + + renderRowColumn (column, row) { + const cls = this.getColumnClasses(column) + const styles = this.getColumnStyles(column) + var content = (column.renderer) ? this[column.renderer](row) : row[column.id] + return html` +
+ ${content} +
+ ` + } + + renderSortIcon (column) { + if (column.id !== this.sortColumn) { + return '' + } + return html` + + ` + } + + renderEmpty () { + // this can be overridden by subclasses + return '' + } + + // events + // = + + onClickHeadingColumn (e, column) { + if (this.sortColumn === column.id) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc' + } else { + this.sortColumn = column.id + this.sortDirection = 'asc' + } + this.sort() + } + + onClickRows (e, row) { + // this can be overridden by subclasses + } + + onClickRow (e, row) { + // this can be overridden by subclasses + } + + onDblclickRow (e, row) { + // this can be overridden by subclasses + } + + onContextmenuRow (e, row) { + // this can be overridden by subclasses + } +} +Table.styles = [tableCSS] diff --git a/app/userland/app-stdlib/js/com/tabs-nav.js b/app/userland/app-stdlib/js/com/tabs-nav.js new file mode 100644 index 0000000000..b7ba377eb2 --- /dev/null +++ b/app/userland/app-stdlib/js/com/tabs-nav.js @@ -0,0 +1,45 @@ +import {LitElement, html} from '../../vendor/lit-element/lit-element.js' +import {classMap} from '../../vendor/lit-element/lit-html/directives/class-map.js' +import {repeat} from '../../vendor/lit-element/lit-html/directives/repeat.js' +import tabsCSS from '../../css/com/tabs-nav.css.js' + +export class TabsNav extends LitElement { + static get properties () { + return { + currentTab: {attribute: 'current-tab', reflect: true} + } + } + + get tabs () { + // this should be overridden by subclasses + return [ + {id: 'tab1', label: 'Tab 1'}, + {id: 'tab2', label: 'Tab 2'}, + {id: 'tab3', label: 'Tab 3'} + ] + } + + constructor () { + super() + this.currentTab = this.tabs[0].id + } + + render () { + return html`${repeat(this.tabs, tab => this.renderTab(tab))}` + } + + renderTab (tab) { + if (tab.spacer) return html`` + if (tab.type === 'html') return tab + + var {id, label, onClick} = tab + const cls = classMap({active: this.currentTab === id}) + return html` this.onClickTab(e, id)}>${label}` + } + + onClickTab (e, id) { + this.currentTab = id + this.dispatchEvent(new CustomEvent('change-tab', {detail: {tab: id}})) + } +} +TabsNav.styles = [tabsCSS] diff --git a/app/userland/app-stdlib/js/com/toast.js b/app/userland/app-stdlib/js/com/toast.js new file mode 100644 index 0000000000..fdeb28e79f --- /dev/null +++ b/app/userland/app-stdlib/js/com/toast.js @@ -0,0 +1,48 @@ +import {LitElement, html} from '../../vendor/lit-element/lit-element.js' +import toastCSS from '../../css/com/toast.css.js' + +// exported api +// = + +export function create (message, type = '', time = 5000, button = null) { + // destroy existing + destroy() + + // render toast + document.body.appendChild(new BeakerToast({message, type, button})) + setTimeout(destroy, time) +} + +// internal +// = + +function destroy () { + var toast = document.querySelector('beaker-toast') + + if (toast) { + // fadeout before removing element + toast.classList.add('hidden') + setTimeout(() => toast.remove(), 500) + } +} + +class BeakerToast extends LitElement { + constructor ({message, type, button}) { + super() + this.message = message + this.type = type + this.button = button + } + + render () { + const onButtonClick = this.button ? (e) => { destroy(); this.button.click(e) } : undefined + return html` +
+

${this.message} ${this.button ? html`${this.button.label}` : ''}

+
+ ` + } +} +BeakerToast.styles = toastCSS + +customElements.define('beaker-toast', BeakerToast) diff --git a/app/userland/app-stdlib/js/com/top-right-controls.js b/app/userland/app-stdlib/js/com/top-right-controls.js new file mode 100644 index 0000000000..2cd62292d2 --- /dev/null +++ b/app/userland/app-stdlib/js/com/top-right-controls.js @@ -0,0 +1,170 @@ +import { LitElement, html, css } from '/vendor/beaker-app-stdlib/vendor/lit-element/lit-element.js' +import * as appMenu from '/vendor/beaker-app-stdlib/js/com/app-menu.js' +import * as contextMenu from '/vendor/beaker-app-stdlib/js/com/context-menu.js' +import * as toast from '/vendor/beaker-app-stdlib/js/com/toast.js' +import _debounce from '../../vendor/lodash.debounce.js' + +const WIKI_KEY = '9d9bc457f39c987cb775e638d1623d894860947509a4143d035305d4d468587b' + +const createContextMenu = (el, items) => contextMenu.create({ + x: el.getBoundingClientRect().right, + y: el.getBoundingClientRect().bottom, + right: true, + withTriangle: true, + noBorders: true, + style: 'padding: 4px 0; min-width: 200px; font-size: 14px; color: #000', + fontAwesomeCSSUrl: '/vendor/beaker-app-stdlib/css/fontawesome.css', + items +}) + +class TopRightControls extends LitElement { + static get properties () { + return { + user: {type: Object}, + cacheBuster: {type: Number} + } + } + + constructor () { + super() + this.user = null + this.cacheBuster = 0 + window.addEventListener('focus', _debounce(() => { + // load latest when we're opened, to make sure we stay in sync + this.cacheBuster = Date.now() + }, 1e3, {leading: true})) + } + + get userName () { + return this.user && this.user.title || 'Anonymous' + } + + get userUrl () { + return this.user ? this.user.url : '' + } + + get userImg () { + return this.user ? html`` : '' + } + + render() { + return html` + + ` + } + + onClickAppMenu (e) { + e.preventDefault() + e.stopPropagation() + appMenu.create({ + x: e.currentTarget.getBoundingClientRect().right, + y: e.currentTarget.getBoundingClientRect().bottom + }) + } + + onClickNewMenu (e) { + e.preventDefault() + e.stopPropagation() + + const goto = (url) => { window.location = url } + async function create (templateUrl, title, description, urlModifyFn) { + toast.create('Loading...', '', 10e3) + setTimeout(() => toast.create('Still loading...', '', 10e3), 10e3) + setTimeout(() => toast.create('Still loading, must be having trouble downloading the template...', '', 10e3), 20e3) + setTimeout(() => toast.create('Okay wow...', '', 10e3), 30e3) + setTimeout(() => toast.create('Still loading, is your Internet connected?...', '', 10e3), 40e3) + setTimeout(() => toast.create('Lets give it 10 more seconds...', '', 10e3), 50e3) + try { + var newSite = await DatArchive.fork(templateUrl, {title, description, prompt: false}) + window.location = urlModifyFn ? urlModifyFn(newSite.url) : newSite.url + } catch (e) { + console.error(e) + if (e.name === 'TimeoutError') { + toast.create('Beaker was unable to download the template for your new site. Please check your Internet connection and try again!', 'error') + } else { + toast.create('Unexpected error: ' + e.message, 'error') + } + } + } + const items = [ + html`
Projects
`, + {icon: false, label: 'Blank website', click: () => goto('beaker://library/?view=new-website')}, + '-', + html`
Templates
`, + {icon: false, label: 'Wiki', click: () => create(WIKI_KEY, 'Untitled Wiki', ' ', url => url + '?edit')}, + ] + createContextMenu(e.currentTarget, items) + } + + onClickProfileMenu (e) { + e.preventDefault() + e.stopPropagation() + + const goto = (url) => { window.location = url } + const items = [ + html`
Identity
`, + {icon: false, label: 'Your personal site', click: () => goto(this.userUrl)}, + '-', + html`
Apps
`, + {icon: false, label: 'Beaker.Social profile', click: () => goto(`intent:unwalled.garden/view-feed?url=${encodeURIComponent(this.userUrl)}`)}, + '-', + html`
Personal data
`, + {icon: false, label: 'Your address book', click: () => goto('beaker://library/?view=addressbook')}, + {icon: false, label: 'Your bookmarks', click: () => goto('beaker://library/?view=bookmarks')}, + {icon: false, label: 'Your websites', click: () => goto('beaker://library/?view=websites')}, + '-', + {icon: false, label: 'Settings', click: () => goto('beaker://settings/')} + ] + createContextMenu(e.currentTarget, items) + } +} + +TopRightControls.styles = css` +div { + display: flex; + align-items: center; + position: fixed; + top: 8px; + right: 10px; + font-size: 16px; +} + +a { + color: gray; + padding: 10px; + cursor: pointer; +} + +a:hover { + color: #555; +} + +.profile { + display: inline-flex; + align-items: center; + font-size: 13px; + border-radius: 2px; + padding: 3px 6px; + margin-left: 5px; +} + +.profile:hover { + color: #333; + background: #eee; +} + +.profile img { + width: 32px; + height: 32px; + object-fit: cover; + border-radius: 50%; + margin-right: 5px; +} +` + +customElements.define('beaker-top-right-controls', TopRightControls) + diff --git a/app/userland/app-stdlib/js/dom.js b/app/userland/app-stdlib/js/dom.js new file mode 100644 index 0000000000..b0f0a1274b --- /dev/null +++ b/app/userland/app-stdlib/js/dom.js @@ -0,0 +1,49 @@ +export function findParent (node, test) { + if (typeof test === 'string') { + // classname default + var cls = test + test = el => el.classList && el.classList.contains(cls) + } + + while (node) { + if (test(node)) { + return node + } + node = node.parentNode + } +} + +export function on (el, event, fn, opts) { + el.addEventListener(event, fn, opts) +} + +export function once (el, event, fn, opts) { + opts = opts || {} + opts.once = true + el.addEventListener(event, fn, opts) +} + +export function emit (el, evt, opts = {}) { + opts.bubbles = ('bubbles' in opts) ? opts.bubbles : true + opts.composed = ('composed' in opts) ? opts.composed : true + el.dispatchEvent(new CustomEvent(evt, opts)) +} + +/*! + * Dynamically changing favicons with JavaScript + * Works in all A-grade browsers except Safari and Internet Explorer + * Demo: http://mathiasbynens.be/demo/dynamic-favicons + */ + +var _head = document.head || document.getElementsByTagName('head')[0]; // https://stackoverflow.com/a/2995536 +export function changeFavicon (src) { + var link = document.createElement('link') + var oldLink = document.getElementById('dynamic-favicon') + link.id = 'dynamic-favicon'; + link.rel = 'shortcut icon'; + link.href = src; + if (oldLink) { + _head.removeChild(oldLink); + } + _head.appendChild(link); +} \ No newline at end of file diff --git a/app/userland/app-stdlib/js/emoji.js b/app/userland/app-stdlib/js/emoji.js new file mode 100644 index 0000000000..c2be479a23 --- /dev/null +++ b/app/userland/app-stdlib/js/emoji.js @@ -0,0 +1,24 @@ +import { FULL_LIST } from '../data/emoji-list.js' +import * as skinTone from '../vendor/emoji-skin-tone/index.js' + +const EMOJI_VARIANT = `\uFE0F` // this codepoint forces emoji rendering rather than symbolic + +export function setSkinTone (emoji, tone) { + return skinTone.set(emoji, tone) +} + +export function render (emoji, tone = false) { + emoji = emoji.replace('\uFE0F', '').replace('\uFE0E', '') + return (tone === false ? emoji : skinTone.set(emoji, tone)) + EMOJI_VARIANT +} + +export function renderSafe (emoji, tone = false) { + // if (!isSupported(emoji)) return '' TODO needed? + return render(emoji, tone) +} + +export function isSupported (emoji) { + if (!emoji || typeof emoji !== 'string') return false + emoji = emoji.replace('\uFE0F', '').replace('\uFE0E', '') + return FULL_LIST.indexOf(skinTone.set(emoji, skinTone.NONE)) !== -1 +} \ No newline at end of file diff --git a/app/userland/app-stdlib/js/query-params.js b/app/userland/app-stdlib/js/query-params.js new file mode 100644 index 0000000000..c6112c0cd8 --- /dev/null +++ b/app/userland/app-stdlib/js/query-params.js @@ -0,0 +1,20 @@ +export function setParams (kv, clear = false, replaceState = false) { + var url = (new URL(window.location)) + if (clear) url.search = '' + for (var k in kv) { + if (kv[k]) { + url.searchParams.set(k, kv[k]) + } else { + url.searchParams.delete(k) + } + } + if (replaceState) { + window.history.replaceState({}, null, url) + } else { + window.history.pushState({}, null, url) + } +} + +export function getParam (k, fallback = '') { + return (new URL(window.location)).searchParams.get(k) || fallback +} \ No newline at end of file diff --git a/app/userland/app-stdlib/js/strings.js b/app/userland/app-stdlib/js/strings.js new file mode 100644 index 0000000000..0cc03d5151 --- /dev/null +++ b/app/userland/app-stdlib/js/strings.js @@ -0,0 +1,78 @@ +export const DAT_KEY_REGEX = /[0-9a-f]{64}/i + +export function ucfirst (str) { + if (!str) str = '' + if (typeof str !== 'string') str = '' + str + return str.charAt(0).toUpperCase() + str.slice(1) +} + +export function pluralize (num, base, suffix = 's') { + if (num === 1) { return base } + return base + suffix +} + +export function shorten (str, n = 6) { + if (str.length > (n + 3)) { + return str.slice(0, n) + '...' + } + return str +} + +export function joinPath (...args) { + var str = args[0] + for (let v of args.slice(1)) { + v = v && typeof v === 'string' ? v : '' + let left = str.endsWith('/') + let right = v.startsWith('/') + if (left !== right) str += v + else if (left) str += v.slice(1) + else str += '/' + v + } + return str +} + +export function toDomain (str) { + if (!str) return '' + try { + var urlParsed = new URL(str) + return urlParsed.hostname + } catch (e) { + // ignore, not a url + } + return str +} + +export function toNiceDomain (str, len=4) { + var domain = toDomain(str) + if (DAT_KEY_REGEX.test(domain)) { + domain = `${domain.slice(0, len)}..${domain.slice(-2)}` + } + return domain +} + +export function toNiceUrl (str) { + if (!str) return '' + try { + var urlParsed = new URL(str) + if (DAT_KEY_REGEX.test(urlParsed.hostname)) { + urlParsed.hostname = `${urlParsed.hostname.slice(0, 4)}..${urlParsed.hostname.slice(-2)}` + } + return urlParsed.toString() + } catch (e) { + // ignore, not a url + } + return str +} + +export function makeSafe (str = '') { + return str.replace(//g, '>').replace(/&/g, '&').replace(/"/g, '"') +} + +// search results are returned from beaker's search APIs with nonces wrapping the highlighted sections +// e.g. a search for "test" might return "the {500}test{/500} result" +// this enables us to safely escape the HTML, then replace the nonces with tags +export function highlightSearchResult (str = '', nonce = 0) { + var start = new RegExp(`\\{${nonce}\\}`, 'g') // eg {500} + var end = new RegExp(`\\{/${nonce}\\}`, 'g') // eg {/500} + return makeSafe(str).replace(start, '').replace(end, '') +} \ No newline at end of file diff --git a/app/userland/app-stdlib/js/time.js b/app/userland/app-stdlib/js/time.js new file mode 100644 index 0000000000..390db59754 --- /dev/null +++ b/app/userland/app-stdlib/js/time.js @@ -0,0 +1,54 @@ +import {pluralize} from './strings.js' + +const shortFormatter = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric' +}) +const longFormatter = new Intl.DateTimeFormat('en-US', { + month: 'short', + year: 'numeric', + day: 'numeric' +}) +const yearFormatter = new Intl.DateTimeFormat('en-US', {year: 'numeric'}) +const CURRENT_YEAR = yearFormatter.format(new Date()) + +export function shortDate (ts) { + ts = new Date(ts) + var year = yearFormatter.format(ts) + var formatter = (year === CURRENT_YEAR) ? shortFormatter : longFormatter + return formatter.format(ts) +} + +// simple timediff fn +// replace this with Intl.RelativeTimeFormat when it lands in Beaker +// https://stackoverflow.com/questions/6108819/javascript-timestamp-to-relative-time-eg-2-seconds-ago-one-week-ago-etc-best +const msPerMinute = 60 * 1000; +const msPerHour = msPerMinute * 60; +const msPerDay = msPerHour * 24; +const msPerMonth = msPerDay * 30; +const msPerYear = msPerDay * 365; +const now = Date.now() +export function timeDifference (ts, short = false, postfix = 'ago') { + ts = Number(new Date(ts)) + var elapsed = now - ts + if (elapsed < 1) elapsed = 1 // let's avoid 0 and negative values + if (elapsed < msPerMinute) { + let n = Math.round(elapsed/1000) + return `${n}${short ? 's' : pluralize(n, ' second')} ${postfix}` + } else if (elapsed < msPerHour) { + let n = Math.round(elapsed/msPerMinute) + return `${n}${short ? 'm' : pluralize(n, ' minute')} ${postfix}` + } else if (elapsed < msPerDay ) { + let n = Math.round(elapsed/msPerHour ) + return `${n}${short ? 'h' : pluralize(n, ' hour')} ${postfix}` + } else if (elapsed < msPerMonth) { + let n = Math.round(elapsed/msPerDay) + return `${n}${short ? 'd' : pluralize(n, ' day')} ${postfix}` + } else if (elapsed < msPerYear) { + let n = Math.round(elapsed/msPerMonth) + return `${n}${short ? 'mo' : pluralize(n, ' month')} ${postfix}` + } else { + let n = Math.round(elapsed/msPerYear ) + return `${n}${short ? 'yr' : pluralize(n, ' year')} ${postfix}` + } +} \ No newline at end of file diff --git a/app/userland/app-stdlib/readme.md b/app/userland/app-stdlib/readme.md new file mode 100644 index 0000000000..e66d3784d5 --- /dev/null +++ b/app/userland/app-stdlib/readme.md @@ -0,0 +1,13 @@ +# Beaker Applications Standard Library + +A collection of JS and Web Components which are used across Beaker's default applications. + +## Scripts + +### Build *.css.js files + +To build the *.css.js files from the original css, run: + +``` +node scripts/generate-css-js.js +``` \ No newline at end of file diff --git a/app/userland/app-stdlib/scripts/css-watcher.js b/app/userland/app-stdlib/scripts/css-watcher.js new file mode 100644 index 0000000000..d299c57d86 --- /dev/null +++ b/app/userland/app-stdlib/scripts/css-watcher.js @@ -0,0 +1,27 @@ +const path = require('path') +const fs = require('fs') +const exec = require('child_process').exec + +fs.watch(path.join(__dirname, '..', 'css'), {recursive: true}, function (eventType, filename) { + if (filename.endsWith('.css')) { + hit() + } +}) + +function run () { + console.log('Change detected, generating...') + exec(`node ${path.join(__dirname, 'generate-css-js.js')}`, (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + return; + } + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + }); +} + +var to = 0 +function hit () { + clearTimeout(to) + to = setTimeout(run, 500) +} \ No newline at end of file diff --git a/app/userland/app-stdlib/scripts/emoji-data.txt b/app/userland/app-stdlib/scripts/emoji-data.txt new file mode 100644 index 0000000000..2ad239f251 --- /dev/null +++ b/app/userland/app-stdlib/scripts/emoji-data.txt @@ -0,0 +1,3303 @@ +# Captured April 17 2019 +# Stripped out all entires which are not fully-qualified +# Commented out all items not yet supported by Beaker +# -prf + + +# emoji-test.txt +# Date: 2019-01-27, 15:43:01 GMT +# © 2019 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see http://www.unicode.org/terms_of_use.html +# +# Emoji Keyboard/Display Test Data for UTS #51 +# Version: 12.0 +# +# For documentation and usage, see http://www.unicode.org/reports/tr51 +# +# This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed. +# Format: code points; status # emoji name +# Code points — list of one or more hex code points, separated by spaces +# Status +# component — an Emoji_Component, +# excluding Regional_Indicators, ASCII, and non-Emoji. +# fully-qualified — a fully-qualified emoji (see ED-18 in UTS #51), +# excluding Emoji_Component +# Notes: +# • This includes the emoji components that need emoji presentation (skin tone and hair) +# when isolated, but omits the components that need not have an emoji +# presentation when isolated. +# • The RGI set is covered by the listed fully-qualified emoji. +# element of the RGI set is missing one or more emoji presentation selectors. +# • The file is in CLDR order, not codepoint order. This is recommended (but not required!) for keyboard palettes. +# • The groups and subgroups are illustrative. See the Emoji Order chart for more information. + + +# group: Smileys & Emotion + +# subgroup: face-smiling +1F600 ; fully-qualified # 😀 grinning face +1F603 ; fully-qualified # 😃 grinning face with big eyes +1F604 ; fully-qualified # 😄 grinning face with smiling eyes +1F601 ; fully-qualified # 😁 beaming face with smiling eyes +1F606 ; fully-qualified # 😆 grinning squinting face +1F605 ; fully-qualified # 😅 grinning face with sweat +1F923 ; fully-qualified # 🤣 rolling on the floor laughing +1F602 ; fully-qualified # 😂 face with tears of joy +1F642 ; fully-qualified # 🙂 slightly smiling face +1F643 ; fully-qualified # 🙃 upside-down face +1F609 ; fully-qualified # 😉 winking face +1F60A ; fully-qualified # 😊 smiling face with smiling eyes +1F607 ; fully-qualified # 😇 smiling face with halo + +# subgroup: face-affection +1F970 ; fully-qualified # 🥰 smiling face with hearts +1F60D ; fully-qualified # 😍 smiling face with heart-eyes +1F929 ; fully-qualified # 🤩 star-struck +1F618 ; fully-qualified # 😘 face blowing a kiss +1F617 ; fully-qualified # 😗 kissing face +#SUPPORT(prf) 263A FE0F ; fully-qualified # ☺️ smiling face +1F61A ; fully-qualified # 😚 kissing face with closed eyes +1F619 ; fully-qualified # 😙 kissing face with smiling eyes + +# subgroup: face-tongue +1F60B ; fully-qualified # 😋 face savoring food +1F61B ; fully-qualified # 😛 face with tongue +1F61C ; fully-qualified # 😜 winking face with tongue +1F92A ; fully-qualified # 🤪 zany face +1F61D ; fully-qualified # 😝 squinting face with tongue +1F911 ; fully-qualified # 🤑 money-mouth face + +# subgroup: face-hand +1F917 ; fully-qualified # 🤗 hugging face +1F92D ; fully-qualified # 🤭 face with hand over mouth +1F92B ; fully-qualified # 🤫 shushing face +1F914 ; fully-qualified # 🤔 thinking face + +# subgroup: face-neutral-skeptical +1F910 ; fully-qualified # 🤐 zipper-mouth face +1F928 ; fully-qualified # 🤨 face with raised eyebrow +1F610 ; fully-qualified # 😐 neutral face +1F611 ; fully-qualified # 😑 expressionless face +1F636 ; fully-qualified # 😶 face without mouth +1F60F ; fully-qualified # 😏 smirking face +1F612 ; fully-qualified # 😒 unamused face +1F644 ; fully-qualified # 🙄 face with rolling eyes +1F62C ; fully-qualified # 😬 grimacing face +1F925 ; fully-qualified # 🤥 lying face + +# subgroup: face-sleepy +1F60C ; fully-qualified # 😌 relieved face +1F614 ; fully-qualified # 😔 pensive face +1F62A ; fully-qualified # 😪 sleepy face +1F924 ; fully-qualified # 🤤 drooling face +1F634 ; fully-qualified # 😴 sleeping face + +# subgroup: face-unwell +1F637 ; fully-qualified # 😷 face with medical mask +1F912 ; fully-qualified # 🤒 face with thermometer +1F915 ; fully-qualified # 🤕 face with head-bandage +1F922 ; fully-qualified # 🤢 nauseated face +1F92E ; fully-qualified # 🤮 face vomiting +1F927 ; fully-qualified # 🤧 sneezing face +1F975 ; fully-qualified # 🥵 hot face +1F976 ; fully-qualified # 🥶 cold face +1F974 ; fully-qualified # 🥴 woozy face +1F635 ; fully-qualified # 😵 dizzy face +1F92F ; fully-qualified # 🤯 exploding head + +# subgroup: face-hat +1F920 ; fully-qualified # 🤠 cowboy hat face +1F973 ; fully-qualified # 🥳 partying face + +# subgroup: face-glasses +1F60E ; fully-qualified # 😎 smiling face with sunglasses +1F913 ; fully-qualified # 🤓 nerd face +1F9D0 ; fully-qualified # 🧐 face with monocle + +# subgroup: face-concerned +1F615 ; fully-qualified # 😕 confused face +1F61F ; fully-qualified # 😟 worried face +1F641 ; fully-qualified # 🙁 slightly frowning face +2639 FE0F ; fully-qualified # ☹️ frowning face +1F62E ; fully-qualified # 😮 face with open mouth +1F62F ; fully-qualified # 😯 hushed face +1F632 ; fully-qualified # 😲 astonished face +1F633 ; fully-qualified # 😳 flushed face +1F97A ; fully-qualified # 🥺 pleading face +1F626 ; fully-qualified # 😦 frowning face with open mouth +1F627 ; fully-qualified # 😧 anguished face +1F628 ; fully-qualified # 😨 fearful face +1F630 ; fully-qualified # 😰 anxious face with sweat +1F625 ; fully-qualified # 😥 sad but relieved face +1F622 ; fully-qualified # 😢 crying face +1F62D ; fully-qualified # 😭 loudly crying face +1F631 ; fully-qualified # 😱 face screaming in fear +1F616 ; fully-qualified # 😖 confounded face +1F623 ; fully-qualified # 😣 persevering face +1F61E ; fully-qualified # 😞 disappointed face +1F613 ; fully-qualified # 😓 downcast face with sweat +1F629 ; fully-qualified # 😩 weary face +1F62B ; fully-qualified # 😫 tired face +#SUPPORT(prf) 1F971 ; fully-qualified # 🥱 yawning face + +# subgroup: face-negative +1F624 ; fully-qualified # 😤 face with steam from nose +1F621 ; fully-qualified # 😡 pouting face +1F620 ; fully-qualified # 😠 angry face +1F92C ; fully-qualified # 🤬 face with symbols on mouth +1F608 ; fully-qualified # 😈 smiling face with horns +1F47F ; fully-qualified # 👿 angry face with horns +1F480 ; fully-qualified # 💀 skull +2620 FE0F ; fully-qualified # ☠️ skull and crossbones + +# subgroup: face-costume +1F4A9 ; fully-qualified # 💩 pile of poo +1F921 ; fully-qualified # 🤡 clown face +1F479 ; fully-qualified # 👹 ogre +1F47A ; fully-qualified # 👺 goblin +1F47B ; fully-qualified # 👻 ghost +1F47D ; fully-qualified # 👽 alien +1F47E ; fully-qualified # 👾 alien monster +1F916 ; fully-qualified # 🤖 robot + +# subgroup: cat-face +1F63A ; fully-qualified # 😺 grinning cat +1F638 ; fully-qualified # 😸 grinning cat with smiling eyes +1F639 ; fully-qualified # 😹 cat with tears of joy +1F63B ; fully-qualified # 😻 smiling cat with heart-eyes +1F63C ; fully-qualified # 😼 cat with wry smile +1F63D ; fully-qualified # 😽 kissing cat +1F640 ; fully-qualified # 🙀 weary cat +1F63F ; fully-qualified # 😿 crying cat +1F63E ; fully-qualified # 😾 pouting cat + +# subgroup: monkey-face +1F648 ; fully-qualified # 🙈 see-no-evil monkey +1F649 ; fully-qualified # 🙉 hear-no-evil monkey +1F64A ; fully-qualified # 🙊 speak-no-evil monkey + +# subgroup: emotion +1F48B ; fully-qualified # 💋 kiss mark +1F48C ; fully-qualified # 💌 love letter +1F498 ; fully-qualified # 💘 heart with arrow +1F49D ; fully-qualified # 💝 heart with ribbon +1F496 ; fully-qualified # 💖 sparkling heart +1F497 ; fully-qualified # 💗 growing heart +1F493 ; fully-qualified # 💓 beating heart +1F49E ; fully-qualified # 💞 revolving hearts +1F495 ; fully-qualified # 💕 two hearts +1F49F ; fully-qualified # 💟 heart decoration +2763 FE0F ; fully-qualified # ❣️ heart exclamation +1F494 ; fully-qualified # 💔 broken heart +2764 FE0F ; fully-qualified # ❤️ red heart +1F9E1 ; fully-qualified # 🧡 orange heart +1F49B ; fully-qualified # 💛 yellow heart +1F49A ; fully-qualified # 💚 green heart +1F499 ; fully-qualified # 💙 blue heart +1F49C ; fully-qualified # 💜 purple heart +#SUPPORT(prf) 1F90E ; fully-qualified # 🤎 brown heart +1F5A4 ; fully-qualified # 🖤 black heart +#SUPPORT(prf) 1F90D ; fully-qualified # 🤍 white heart +1F4AF ; fully-qualified # 💯 hundred points +1F4A2 ; fully-qualified # 💢 anger symbol +1F4A5 ; fully-qualified # 💥 collision +1F4AB ; fully-qualified # 💫 dizzy +1F4A6 ; fully-qualified # 💦 sweat droplets +1F4A8 ; fully-qualified # 💨 dashing away +1F573 FE0F ; fully-qualified # 🕳️ hole +1F4A3 ; fully-qualified # 💣 bomb +1F4AC ; fully-qualified # 💬 speech balloon +1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️‍🗨️ eye in speech bubble +1F5E8 FE0F ; fully-qualified # 🗨️ left speech bubble +1F5EF FE0F ; fully-qualified # 🗯️ right anger bubble +1F4AD ; fully-qualified # 💭 thought balloon +1F4A4 ; fully-qualified # 💤 zzz + +# Smileys & Emotion subtotal: 160 +# Smileys & Emotion subtotal: 160 w/o modifiers + +# group: People & Body + +# subgroup: hand-fingers-open +1F44B ; fully-qualified # 👋 waving hand +1F44B 1F3FB ; fully-qualified # 👋🏻 waving hand: light skin tone +1F44B 1F3FC ; fully-qualified # 👋🏼 waving hand: medium-light skin tone +1F44B 1F3FD ; fully-qualified # 👋🏽 waving hand: medium skin tone +1F44B 1F3FE ; fully-qualified # 👋🏾 waving hand: medium-dark skin tone +1F44B 1F3FF ; fully-qualified # 👋🏿 waving hand: dark skin tone +1F91A ; fully-qualified # 🤚 raised back of hand +1F91A 1F3FB ; fully-qualified # 🤚🏻 raised back of hand: light skin tone +1F91A 1F3FC ; fully-qualified # 🤚🏼 raised back of hand: medium-light skin tone +1F91A 1F3FD ; fully-qualified # 🤚🏽 raised back of hand: medium skin tone +1F91A 1F3FE ; fully-qualified # 🤚🏾 raised back of hand: medium-dark skin tone +1F91A 1F3FF ; fully-qualified # 🤚🏿 raised back of hand: dark skin tone +1F590 FE0F ; fully-qualified # 🖐️ hand with fingers splayed +1F590 1F3FB ; fully-qualified # 🖐🏻 hand with fingers splayed: light skin tone +1F590 1F3FC ; fully-qualified # 🖐🏼 hand with fingers splayed: medium-light skin tone +1F590 1F3FD ; fully-qualified # 🖐🏽 hand with fingers splayed: medium skin tone +1F590 1F3FE ; fully-qualified # 🖐🏾 hand with fingers splayed: medium-dark skin tone +1F590 1F3FF ; fully-qualified # 🖐🏿 hand with fingers splayed: dark skin tone +270B ; fully-qualified # ✋ raised hand +270B 1F3FB ; fully-qualified # ✋🏻 raised hand: light skin tone +270B 1F3FC ; fully-qualified # ✋🏼 raised hand: medium-light skin tone +270B 1F3FD ; fully-qualified # ✋🏽 raised hand: medium skin tone +270B 1F3FE ; fully-qualified # ✋🏾 raised hand: medium-dark skin tone +270B 1F3FF ; fully-qualified # ✋🏿 raised hand: dark skin tone +1F596 ; fully-qualified # 🖖 vulcan salute +1F596 1F3FB ; fully-qualified # 🖖🏻 vulcan salute: light skin tone +1F596 1F3FC ; fully-qualified # 🖖🏼 vulcan salute: medium-light skin tone +1F596 1F3FD ; fully-qualified # 🖖🏽 vulcan salute: medium skin tone +1F596 1F3FE ; fully-qualified # 🖖🏾 vulcan salute: medium-dark skin tone +1F596 1F3FF ; fully-qualified # 🖖🏿 vulcan salute: dark skin tone + +# subgroup: hand-fingers-partial +1F44C ; fully-qualified # 👌 OK hand +1F44C 1F3FB ; fully-qualified # 👌🏻 OK hand: light skin tone +1F44C 1F3FC ; fully-qualified # 👌🏼 OK hand: medium-light skin tone +1F44C 1F3FD ; fully-qualified # 👌🏽 OK hand: medium skin tone +1F44C 1F3FE ; fully-qualified # 👌🏾 OK hand: medium-dark skin tone +1F44C 1F3FF ; fully-qualified # 👌🏿 OK hand: dark skin tone +#SUPPORT(prf) 1F90F ; fully-qualified # 🤏 pinching hand +#SUPPORT(prf) 1F90F 1F3FB ; fully-qualified # 🤏🏻 pinching hand: light skin tone +#SUPPORT(prf) 1F90F 1F3FC ; fully-qualified # 🤏🏼 pinching hand: medium-light skin tone +#SUPPORT(prf) 1F90F 1F3FD ; fully-qualified # 🤏🏽 pinching hand: medium skin tone +#SUPPORT(prf) 1F90F 1F3FE ; fully-qualified # 🤏🏾 pinching hand: medium-dark skin tone +#SUPPORT(prf) 1F90F 1F3FF ; fully-qualified # 🤏🏿 pinching hand: dark skin tone +270C FE0F ; fully-qualified # ✌️ victory hand +270C 1F3FB ; fully-qualified # ✌🏻 victory hand: light skin tone +270C 1F3FC ; fully-qualified # ✌🏼 victory hand: medium-light skin tone +270C 1F3FD ; fully-qualified # ✌🏽 victory hand: medium skin tone +270C 1F3FE ; fully-qualified # ✌🏾 victory hand: medium-dark skin tone +270C 1F3FF ; fully-qualified # ✌🏿 victory hand: dark skin tone +1F91E ; fully-qualified # 🤞 crossed fingers +1F91E 1F3FB ; fully-qualified # 🤞🏻 crossed fingers: light skin tone +1F91E 1F3FC ; fully-qualified # 🤞🏼 crossed fingers: medium-light skin tone +1F91E 1F3FD ; fully-qualified # 🤞🏽 crossed fingers: medium skin tone +1F91E 1F3FE ; fully-qualified # 🤞🏾 crossed fingers: medium-dark skin tone +1F91E 1F3FF ; fully-qualified # 🤞🏿 crossed fingers: dark skin tone +1F91F ; fully-qualified # 🤟 love-you gesture +1F91F 1F3FB ; fully-qualified # 🤟🏻 love-you gesture: light skin tone +1F91F 1F3FC ; fully-qualified # 🤟🏼 love-you gesture: medium-light skin tone +1F91F 1F3FD ; fully-qualified # 🤟🏽 love-you gesture: medium skin tone +1F91F 1F3FE ; fully-qualified # 🤟🏾 love-you gesture: medium-dark skin tone +1F91F 1F3FF ; fully-qualified # 🤟🏿 love-you gesture: dark skin tone +1F918 ; fully-qualified # 🤘 sign of the horns +1F918 1F3FB ; fully-qualified # 🤘🏻 sign of the horns: light skin tone +1F918 1F3FC ; fully-qualified # 🤘🏼 sign of the horns: medium-light skin tone +1F918 1F3FD ; fully-qualified # 🤘🏽 sign of the horns: medium skin tone +1F918 1F3FE ; fully-qualified # 🤘🏾 sign of the horns: medium-dark skin tone +1F918 1F3FF ; fully-qualified # 🤘🏿 sign of the horns: dark skin tone +1F919 ; fully-qualified # 🤙 call me hand +1F919 1F3FB ; fully-qualified # 🤙🏻 call me hand: light skin tone +1F919 1F3FC ; fully-qualified # 🤙🏼 call me hand: medium-light skin tone +1F919 1F3FD ; fully-qualified # 🤙🏽 call me hand: medium skin tone +1F919 1F3FE ; fully-qualified # 🤙🏾 call me hand: medium-dark skin tone +1F919 1F3FF ; fully-qualified # 🤙🏿 call me hand: dark skin tone + +# subgroup: hand-single-finger +1F448 ; fully-qualified # 👈 backhand index pointing left +1F448 1F3FB ; fully-qualified # 👈🏻 backhand index pointing left: light skin tone +1F448 1F3FC ; fully-qualified # 👈🏼 backhand index pointing left: medium-light skin tone +1F448 1F3FD ; fully-qualified # 👈🏽 backhand index pointing left: medium skin tone +1F448 1F3FE ; fully-qualified # 👈🏾 backhand index pointing left: medium-dark skin tone +1F448 1F3FF ; fully-qualified # 👈🏿 backhand index pointing left: dark skin tone +1F449 ; fully-qualified # 👉 backhand index pointing right +1F449 1F3FB ; fully-qualified # 👉🏻 backhand index pointing right: light skin tone +1F449 1F3FC ; fully-qualified # 👉🏼 backhand index pointing right: medium-light skin tone +1F449 1F3FD ; fully-qualified # 👉🏽 backhand index pointing right: medium skin tone +1F449 1F3FE ; fully-qualified # 👉🏾 backhand index pointing right: medium-dark skin tone +1F449 1F3FF ; fully-qualified # 👉🏿 backhand index pointing right: dark skin tone +1F446 ; fully-qualified # 👆 backhand index pointing up +1F446 1F3FB ; fully-qualified # 👆🏻 backhand index pointing up: light skin tone +1F446 1F3FC ; fully-qualified # 👆🏼 backhand index pointing up: medium-light skin tone +1F446 1F3FD ; fully-qualified # 👆🏽 backhand index pointing up: medium skin tone +1F446 1F3FE ; fully-qualified # 👆🏾 backhand index pointing up: medium-dark skin tone +1F446 1F3FF ; fully-qualified # 👆🏿 backhand index pointing up: dark skin tone +1F595 ; fully-qualified # 🖕 middle finger +1F595 1F3FB ; fully-qualified # 🖕🏻 middle finger: light skin tone +1F595 1F3FC ; fully-qualified # 🖕🏼 middle finger: medium-light skin tone +1F595 1F3FD ; fully-qualified # 🖕🏽 middle finger: medium skin tone +1F595 1F3FE ; fully-qualified # 🖕🏾 middle finger: medium-dark skin tone +1F595 1F3FF ; fully-qualified # 🖕🏿 middle finger: dark skin tone +1F447 ; fully-qualified # 👇 backhand index pointing down +1F447 1F3FB ; fully-qualified # 👇🏻 backhand index pointing down: light skin tone +1F447 1F3FC ; fully-qualified # 👇🏼 backhand index pointing down: medium-light skin tone +1F447 1F3FD ; fully-qualified # 👇🏽 backhand index pointing down: medium skin tone +1F447 1F3FE ; fully-qualified # 👇🏾 backhand index pointing down: medium-dark skin tone +1F447 1F3FF ; fully-qualified # 👇🏿 backhand index pointing down: dark skin tone +261D FE0F ; fully-qualified # ☝️ index pointing up +261D 1F3FB ; fully-qualified # ☝🏻 index pointing up: light skin tone +261D 1F3FC ; fully-qualified # ☝🏼 index pointing up: medium-light skin tone +261D 1F3FD ; fully-qualified # ☝🏽 index pointing up: medium skin tone +261D 1F3FE ; fully-qualified # ☝🏾 index pointing up: medium-dark skin tone +261D 1F3FF ; fully-qualified # ☝🏿 index pointing up: dark skin tone + +# subgroup: hand-fingers-closed +1F44D ; fully-qualified # 👍 thumbs up +1F44D 1F3FB ; fully-qualified # 👍🏻 thumbs up: light skin tone +1F44D 1F3FC ; fully-qualified # 👍🏼 thumbs up: medium-light skin tone +1F44D 1F3FD ; fully-qualified # 👍🏽 thumbs up: medium skin tone +1F44D 1F3FE ; fully-qualified # 👍🏾 thumbs up: medium-dark skin tone +1F44D 1F3FF ; fully-qualified # 👍🏿 thumbs up: dark skin tone +1F44E ; fully-qualified # 👎 thumbs down +1F44E 1F3FB ; fully-qualified # 👎🏻 thumbs down: light skin tone +1F44E 1F3FC ; fully-qualified # 👎🏼 thumbs down: medium-light skin tone +1F44E 1F3FD ; fully-qualified # 👎🏽 thumbs down: medium skin tone +1F44E 1F3FE ; fully-qualified # 👎🏾 thumbs down: medium-dark skin tone +1F44E 1F3FF ; fully-qualified # 👎🏿 thumbs down: dark skin tone +270A ; fully-qualified # ✊ raised fist +270A 1F3FB ; fully-qualified # ✊🏻 raised fist: light skin tone +270A 1F3FC ; fully-qualified # ✊🏼 raised fist: medium-light skin tone +270A 1F3FD ; fully-qualified # ✊🏽 raised fist: medium skin tone +270A 1F3FE ; fully-qualified # ✊🏾 raised fist: medium-dark skin tone +270A 1F3FF ; fully-qualified # ✊🏿 raised fist: dark skin tone +1F44A ; fully-qualified # 👊 oncoming fist +1F44A 1F3FB ; fully-qualified # 👊🏻 oncoming fist: light skin tone +1F44A 1F3FC ; fully-qualified # 👊🏼 oncoming fist: medium-light skin tone +1F44A 1F3FD ; fully-qualified # 👊🏽 oncoming fist: medium skin tone +1F44A 1F3FE ; fully-qualified # 👊🏾 oncoming fist: medium-dark skin tone +1F44A 1F3FF ; fully-qualified # 👊🏿 oncoming fist: dark skin tone +1F91B ; fully-qualified # 🤛 left-facing fist +1F91B 1F3FB ; fully-qualified # 🤛🏻 left-facing fist: light skin tone +1F91B 1F3FC ; fully-qualified # 🤛🏼 left-facing fist: medium-light skin tone +1F91B 1F3FD ; fully-qualified # 🤛🏽 left-facing fist: medium skin tone +1F91B 1F3FE ; fully-qualified # 🤛🏾 left-facing fist: medium-dark skin tone +1F91B 1F3FF ; fully-qualified # 🤛🏿 left-facing fist: dark skin tone +1F91C ; fully-qualified # 🤜 right-facing fist +1F91C 1F3FB ; fully-qualified # 🤜🏻 right-facing fist: light skin tone +1F91C 1F3FC ; fully-qualified # 🤜🏼 right-facing fist: medium-light skin tone +1F91C 1F3FD ; fully-qualified # 🤜🏽 right-facing fist: medium skin tone +1F91C 1F3FE ; fully-qualified # 🤜🏾 right-facing fist: medium-dark skin tone +1F91C 1F3FF ; fully-qualified # 🤜🏿 right-facing fist: dark skin tone + +# subgroup: hands +1F44F ; fully-qualified # 👏 clapping hands +1F44F 1F3FB ; fully-qualified # 👏🏻 clapping hands: light skin tone +1F44F 1F3FC ; fully-qualified # 👏🏼 clapping hands: medium-light skin tone +1F44F 1F3FD ; fully-qualified # 👏🏽 clapping hands: medium skin tone +1F44F 1F3FE ; fully-qualified # 👏🏾 clapping hands: medium-dark skin tone +1F44F 1F3FF ; fully-qualified # 👏🏿 clapping hands: dark skin tone +1F64C ; fully-qualified # 🙌 raising hands +1F64C 1F3FB ; fully-qualified # 🙌🏻 raising hands: light skin tone +1F64C 1F3FC ; fully-qualified # 🙌🏼 raising hands: medium-light skin tone +1F64C 1F3FD ; fully-qualified # 🙌🏽 raising hands: medium skin tone +1F64C 1F3FE ; fully-qualified # 🙌🏾 raising hands: medium-dark skin tone +1F64C 1F3FF ; fully-qualified # 🙌🏿 raising hands: dark skin tone +1F450 ; fully-qualified # 👐 open hands +1F450 1F3FB ; fully-qualified # 👐🏻 open hands: light skin tone +1F450 1F3FC ; fully-qualified # 👐🏼 open hands: medium-light skin tone +1F450 1F3FD ; fully-qualified # 👐🏽 open hands: medium skin tone +1F450 1F3FE ; fully-qualified # 👐🏾 open hands: medium-dark skin tone +1F450 1F3FF ; fully-qualified # 👐🏿 open hands: dark skin tone +1F932 ; fully-qualified # 🤲 palms up together +1F932 1F3FB ; fully-qualified # 🤲🏻 palms up together: light skin tone +1F932 1F3FC ; fully-qualified # 🤲🏼 palms up together: medium-light skin tone +1F932 1F3FD ; fully-qualified # 🤲🏽 palms up together: medium skin tone +1F932 1F3FE ; fully-qualified # 🤲🏾 palms up together: medium-dark skin tone +1F932 1F3FF ; fully-qualified # 🤲🏿 palms up together: dark skin tone +1F91D ; fully-qualified # 🤝 handshake +1F64F ; fully-qualified # 🙏 folded hands +1F64F 1F3FB ; fully-qualified # 🙏🏻 folded hands: light skin tone +1F64F 1F3FC ; fully-qualified # 🙏🏼 folded hands: medium-light skin tone +1F64F 1F3FD ; fully-qualified # 🙏🏽 folded hands: medium skin tone +1F64F 1F3FE ; fully-qualified # 🙏🏾 folded hands: medium-dark skin tone +1F64F 1F3FF ; fully-qualified # 🙏🏿 folded hands: dark skin tone + +# subgroup: hand-prop +270D FE0F ; fully-qualified # ✍️ writing hand +270D 1F3FB ; fully-qualified # ✍🏻 writing hand: light skin tone +270D 1F3FC ; fully-qualified # ✍🏼 writing hand: medium-light skin tone +270D 1F3FD ; fully-qualified # ✍🏽 writing hand: medium skin tone +270D 1F3FE ; fully-qualified # ✍🏾 writing hand: medium-dark skin tone +270D 1F3FF ; fully-qualified # ✍🏿 writing hand: dark skin tone +1F485 ; fully-qualified # 💅 nail polish +1F485 1F3FB ; fully-qualified # 💅🏻 nail polish: light skin tone +1F485 1F3FC ; fully-qualified # 💅🏼 nail polish: medium-light skin tone +1F485 1F3FD ; fully-qualified # 💅🏽 nail polish: medium skin tone +1F485 1F3FE ; fully-qualified # 💅🏾 nail polish: medium-dark skin tone +1F485 1F3FF ; fully-qualified # 💅🏿 nail polish: dark skin tone +1F933 ; fully-qualified # 🤳 selfie +1F933 1F3FB ; fully-qualified # 🤳🏻 selfie: light skin tone +1F933 1F3FC ; fully-qualified # 🤳🏼 selfie: medium-light skin tone +1F933 1F3FD ; fully-qualified # 🤳🏽 selfie: medium skin tone +1F933 1F3FE ; fully-qualified # 🤳🏾 selfie: medium-dark skin tone +1F933 1F3FF ; fully-qualified # 🤳🏿 selfie: dark skin tone + +# subgroup: body-parts +1F4AA ; fully-qualified # 💪 flexed biceps +1F4AA 1F3FB ; fully-qualified # 💪🏻 flexed biceps: light skin tone +1F4AA 1F3FC ; fully-qualified # 💪🏼 flexed biceps: medium-light skin tone +1F4AA 1F3FD ; fully-qualified # 💪🏽 flexed biceps: medium skin tone +1F4AA 1F3FE ; fully-qualified # 💪🏾 flexed biceps: medium-dark skin tone +1F4AA 1F3FF ; fully-qualified # 💪🏿 flexed biceps: dark skin tone +#SUPPORT(prf) 1F9BE ; fully-qualified # 🦾 mechanical arm +#SUPPORT(prf) 1F9BF ; fully-qualified # 🦿 mechanical leg +1F9B5 ; fully-qualified # 🦵 leg +1F9B5 1F3FB ; fully-qualified # 🦵🏻 leg: light skin tone +1F9B5 1F3FC ; fully-qualified # 🦵🏼 leg: medium-light skin tone +1F9B5 1F3FD ; fully-qualified # 🦵🏽 leg: medium skin tone +1F9B5 1F3FE ; fully-qualified # 🦵🏾 leg: medium-dark skin tone +1F9B5 1F3FF ; fully-qualified # 🦵🏿 leg: dark skin tone +1F9B6 ; fully-qualified # 🦶 foot +1F9B6 1F3FB ; fully-qualified # 🦶🏻 foot: light skin tone +1F9B6 1F3FC ; fully-qualified # 🦶🏼 foot: medium-light skin tone +1F9B6 1F3FD ; fully-qualified # 🦶🏽 foot: medium skin tone +1F9B6 1F3FE ; fully-qualified # 🦶🏾 foot: medium-dark skin tone +1F9B6 1F3FF ; fully-qualified # 🦶🏿 foot: dark skin tone +1F442 ; fully-qualified # 👂 ear +1F442 1F3FB ; fully-qualified # 👂🏻 ear: light skin tone +1F442 1F3FC ; fully-qualified # 👂🏼 ear: medium-light skin tone +1F442 1F3FD ; fully-qualified # 👂🏽 ear: medium skin tone +1F442 1F3FE ; fully-qualified # 👂🏾 ear: medium-dark skin tone +1F442 1F3FF ; fully-qualified # 👂🏿 ear: dark skin tone +#SUPPORT(prf) 1F9BB ; fully-qualified # 🦻 ear with hearing aid +#SUPPORT(prf) 1F9BB 1F3FB ; fully-qualified # 🦻🏻 ear with hearing aid: light skin tone +#SUPPORT(prf) 1F9BB 1F3FC ; fully-qualified # 🦻🏼 ear with hearing aid: medium-light skin tone +#SUPPORT(prf) 1F9BB 1F3FD ; fully-qualified # 🦻🏽 ear with hearing aid: medium skin tone +#SUPPORT(prf) 1F9BB 1F3FE ; fully-qualified # 🦻🏾 ear with hearing aid: medium-dark skin tone +#SUPPORT(prf) 1F9BB 1F3FF ; fully-qualified # 🦻🏿 ear with hearing aid: dark skin tone +1F443 ; fully-qualified # 👃 nose +1F443 1F3FB ; fully-qualified # 👃🏻 nose: light skin tone +1F443 1F3FC ; fully-qualified # 👃🏼 nose: medium-light skin tone +1F443 1F3FD ; fully-qualified # 👃🏽 nose: medium skin tone +1F443 1F3FE ; fully-qualified # 👃🏾 nose: medium-dark skin tone +1F443 1F3FF ; fully-qualified # 👃🏿 nose: dark skin tone +1F9E0 ; fully-qualified # 🧠 brain +1F9B7 ; fully-qualified # 🦷 tooth +1F9B4 ; fully-qualified # 🦴 bone +1F440 ; fully-qualified # 👀 eyes +1F441 FE0F ; fully-qualified # 👁️ eye +1F445 ; fully-qualified # 👅 tongue +1F444 ; fully-qualified # 👄 mouth + +# subgroup: person +1F476 ; fully-qualified # 👶 baby +1F476 1F3FB ; fully-qualified # 👶🏻 baby: light skin tone +1F476 1F3FC ; fully-qualified # 👶🏼 baby: medium-light skin tone +1F476 1F3FD ; fully-qualified # 👶🏽 baby: medium skin tone +1F476 1F3FE ; fully-qualified # 👶🏾 baby: medium-dark skin tone +1F476 1F3FF ; fully-qualified # 👶🏿 baby: dark skin tone +1F9D2 ; fully-qualified # 🧒 child +1F9D2 1F3FB ; fully-qualified # 🧒🏻 child: light skin tone +1F9D2 1F3FC ; fully-qualified # 🧒🏼 child: medium-light skin tone +1F9D2 1F3FD ; fully-qualified # 🧒🏽 child: medium skin tone +1F9D2 1F3FE ; fully-qualified # 🧒🏾 child: medium-dark skin tone +1F9D2 1F3FF ; fully-qualified # 🧒🏿 child: dark skin tone +1F466 ; fully-qualified # 👦 boy +1F466 1F3FB ; fully-qualified # 👦🏻 boy: light skin tone +1F466 1F3FC ; fully-qualified # 👦🏼 boy: medium-light skin tone +1F466 1F3FD ; fully-qualified # 👦🏽 boy: medium skin tone +1F466 1F3FE ; fully-qualified # 👦🏾 boy: medium-dark skin tone +1F466 1F3FF ; fully-qualified # 👦🏿 boy: dark skin tone +1F467 ; fully-qualified # 👧 girl +1F467 1F3FB ; fully-qualified # 👧🏻 girl: light skin tone +1F467 1F3FC ; fully-qualified # 👧🏼 girl: medium-light skin tone +1F467 1F3FD ; fully-qualified # 👧🏽 girl: medium skin tone +1F467 1F3FE ; fully-qualified # 👧🏾 girl: medium-dark skin tone +1F467 1F3FF ; fully-qualified # 👧🏿 girl: dark skin tone +1F9D1 ; fully-qualified # 🧑 person +1F9D1 1F3FB ; fully-qualified # 🧑🏻 person: light skin tone +1F9D1 1F3FC ; fully-qualified # 🧑🏼 person: medium-light skin tone +1F9D1 1F3FD ; fully-qualified # 🧑🏽 person: medium skin tone +1F9D1 1F3FE ; fully-qualified # 🧑🏾 person: medium-dark skin tone +1F9D1 1F3FF ; fully-qualified # 🧑🏿 person: dark skin tone +1F471 ; fully-qualified # 👱 person: blond hair +1F471 1F3FB ; fully-qualified # 👱🏻 person: light skin tone, blond hair +1F471 1F3FC ; fully-qualified # 👱🏼 person: medium-light skin tone, blond hair +1F471 1F3FD ; fully-qualified # 👱🏽 person: medium skin tone, blond hair +1F471 1F3FE ; fully-qualified # 👱🏾 person: medium-dark skin tone, blond hair +1F471 1F3FF ; fully-qualified # 👱🏿 person: dark skin tone, blond hair +1F468 ; fully-qualified # 👨 man +1F468 1F3FB ; fully-qualified # 👨🏻 man: light skin tone +1F468 1F3FC ; fully-qualified # 👨🏼 man: medium-light skin tone +1F468 1F3FD ; fully-qualified # 👨🏽 man: medium skin tone +1F468 1F3FE ; fully-qualified # 👨🏾 man: medium-dark skin tone +1F468 1F3FF ; fully-qualified # 👨🏿 man: dark skin tone +1F9D4 ; fully-qualified # 🧔 man: beard +1F9D4 1F3FB ; fully-qualified # 🧔🏻 man: light skin tone, beard +1F9D4 1F3FC ; fully-qualified # 🧔🏼 man: medium-light skin tone, beard +1F9D4 1F3FD ; fully-qualified # 🧔🏽 man: medium skin tone, beard +1F9D4 1F3FE ; fully-qualified # 🧔🏾 man: medium-dark skin tone, beard +1F9D4 1F3FF ; fully-qualified # 🧔🏿 man: dark skin tone, beard +#SUPPORT(prf) 1F471 200D 2642 FE0F ; fully-qualified # 👱‍♂️ man: blond hair +#SUPPORT(prf) 1F471 1F3FB 200D 2642 FE0F ; fully-qualified # 👱🏻‍♂️ man: light skin tone, blond hair +#SUPPORT(prf) 1F471 1F3FC 200D 2642 FE0F ; fully-qualified # 👱🏼‍♂️ man: medium-light skin tone, blond hair +#SUPPORT(prf) 1F471 1F3FD 200D 2642 FE0F ; fully-qualified # 👱🏽‍♂️ man: medium skin tone, blond hair +#SUPPORT(prf) 1F471 1F3FE 200D 2642 FE0F ; fully-qualified # 👱🏾‍♂️ man: medium-dark skin tone, blond hair +#SUPPORT(prf) 1F471 1F3FF 200D 2642 FE0F ; fully-qualified # 👱🏿‍♂️ man: dark skin tone, blond hair +#SUPPORT(prf) 1F468 200D 1F9B0 ; fully-qualified # 👨‍🦰 man: red hair +#SUPPORT(prf) 1F468 1F3FB 200D 1F9B0 ; fully-qualified # 👨🏻‍🦰 man: light skin tone, red hair +#SUPPORT(prf) 1F468 1F3FC 200D 1F9B0 ; fully-qualified # 👨🏼‍🦰 man: medium-light skin tone, red hair +#SUPPORT(prf) 1F468 1F3FD 200D 1F9B0 ; fully-qualified # 👨🏽‍🦰 man: medium skin tone, red hair +#SUPPORT(prf) 1F468 1F3FE 200D 1F9B0 ; fully-qualified # 👨🏾‍🦰 man: medium-dark skin tone, red hair +#SUPPORT(prf) 1F468 1F3FF 200D 1F9B0 ; fully-qualified # 👨🏿‍🦰 man: dark skin tone, red hair +#SUPPORT(prf) 1F468 200D 1F9B1 ; fully-qualified # 👨‍🦱 man: curly hair +#SUPPORT(prf) 1F468 1F3FB 200D 1F9B1 ; fully-qualified # 👨🏻‍🦱 man: light skin tone, curly hair +#SUPPORT(prf) 1F468 1F3FC 200D 1F9B1 ; fully-qualified # 👨🏼‍🦱 man: medium-light skin tone, curly hair +#SUPPORT(prf) 1F468 1F3FD 200D 1F9B1 ; fully-qualified # 👨🏽‍🦱 man: medium skin tone, curly hair +#SUPPORT(prf) 1F468 1F3FE 200D 1F9B1 ; fully-qualified # 👨🏾‍🦱 man: medium-dark skin tone, curly hair +#SUPPORT(prf) 1F468 1F3FF 200D 1F9B1 ; fully-qualified # 👨🏿‍🦱 man: dark skin tone, curly hair +#SUPPORT(prf) 1F468 200D 1F9B3 ; fully-qualified # 👨‍🦳 man: white hair +#SUPPORT(prf) 1F468 1F3FB 200D 1F9B3 ; fully-qualified # 👨🏻‍🦳 man: light skin tone, white hair +#SUPPORT(prf) 1F468 1F3FC 200D 1F9B3 ; fully-qualified # 👨🏼‍🦳 man: medium-light skin tone, white hair +#SUPPORT(prf) 1F468 1F3FD 200D 1F9B3 ; fully-qualified # 👨🏽‍🦳 man: medium skin tone, white hair +#SUPPORT(prf) 1F468 1F3FE 200D 1F9B3 ; fully-qualified # 👨🏾‍🦳 man: medium-dark skin tone, white hair +#SUPPORT(prf) 1F468 1F3FF 200D 1F9B3 ; fully-qualified # 👨🏿‍🦳 man: dark skin tone, white hair +#SUPPORT(prf) 1F468 200D 1F9B2 ; fully-qualified # 👨‍🦲 man: bald +#SUPPORT(prf) 1F468 1F3FB 200D 1F9B2 ; fully-qualified # 👨🏻‍🦲 man: light skin tone, bald +#SUPPORT(prf) 1F468 1F3FC 200D 1F9B2 ; fully-qualified # 👨🏼‍🦲 man: medium-light skin tone, bald +#SUPPORT(prf) 1F468 1F3FD 200D 1F9B2 ; fully-qualified # 👨🏽‍🦲 man: medium skin tone, bald +#SUPPORT(prf) 1F468 1F3FE 200D 1F9B2 ; fully-qualified # 👨🏾‍🦲 man: medium-dark skin tone, bald +#SUPPORT(prf) 1F468 1F3FF 200D 1F9B2 ; fully-qualified # 👨🏿‍🦲 man: dark skin tone, bald +1F469 ; fully-qualified # 👩 woman +1F469 1F3FB ; fully-qualified # 👩🏻 woman: light skin tone +1F469 1F3FC ; fully-qualified # 👩🏼 woman: medium-light skin tone +1F469 1F3FD ; fully-qualified # 👩🏽 woman: medium skin tone +1F469 1F3FE ; fully-qualified # 👩🏾 woman: medium-dark skin tone +1F469 1F3FF ; fully-qualified # 👩🏿 woman: dark skin tone +#SUPPORT(prf) 1F471 200D 2640 FE0F ; fully-qualified # 👱‍♀️ woman: blond hair +#SUPPORT(prf) 1F471 1F3FB 200D 2640 FE0F ; fully-qualified # 👱🏻‍♀️ woman: light skin tone, blond hair +#SUPPORT(prf) 1F471 1F3FC 200D 2640 FE0F ; fully-qualified # 👱🏼‍♀️ woman: medium-light skin tone, blond hair +#SUPPORT(prf) 1F471 1F3FD 200D 2640 FE0F ; fully-qualified # 👱🏽‍♀️ woman: medium skin tone, blond hair +#SUPPORT(prf) 1F471 1F3FE 200D 2640 FE0F ; fully-qualified # 👱🏾‍♀️ woman: medium-dark skin tone, blond hair +#SUPPORT(prf) 1F471 1F3FF 200D 2640 FE0F ; fully-qualified # 👱🏿‍♀️ woman: dark skin tone, blond hair +#SUPPORT(prf) 1F469 200D 1F9B0 ; fully-qualified # 👩‍🦰 woman: red hair +#SUPPORT(prf) 1F469 1F3FB 200D 1F9B0 ; fully-qualified # 👩🏻‍🦰 woman: light skin tone, red hair +#SUPPORT(prf) 1F469 1F3FC 200D 1F9B0 ; fully-qualified # 👩🏼‍🦰 woman: medium-light skin tone, red hair +#SUPPORT(prf) 1F469 1F3FD 200D 1F9B0 ; fully-qualified # 👩🏽‍🦰 woman: medium skin tone, red hair +#SUPPORT(prf) 1F469 1F3FE 200D 1F9B0 ; fully-qualified # 👩🏾‍🦰 woman: medium-dark skin tone, red hair +#SUPPORT(prf) 1F469 1F3FF 200D 1F9B0 ; fully-qualified # 👩🏿‍🦰 woman: dark skin tone, red hair +#SUPPORT(prf) 1F469 200D 1F9B1 ; fully-qualified # 👩‍🦱 woman: curly hair +#SUPPORT(prf) 1F469 1F3FB 200D 1F9B1 ; fully-qualified # 👩🏻‍🦱 woman: light skin tone, curly hair +#SUPPORT(prf) 1F469 1F3FC 200D 1F9B1 ; fully-qualified # 👩🏼‍🦱 woman: medium-light skin tone, curly hair +#SUPPORT(prf) 1F469 1F3FD 200D 1F9B1 ; fully-qualified # 👩🏽‍🦱 woman: medium skin tone, curly hair +#SUPPORT(prf) 1F469 1F3FE 200D 1F9B1 ; fully-qualified # 👩🏾‍🦱 woman: medium-dark skin tone, curly hair +#SUPPORT(prf) 1F469 1F3FF 200D 1F9B1 ; fully-qualified # 👩🏿‍🦱 woman: dark skin tone, curly hair +#SUPPORT(prf) 1F469 200D 1F9B3 ; fully-qualified # 👩‍🦳 woman: white hair +#SUPPORT(prf) 1F469 1F3FB 200D 1F9B3 ; fully-qualified # 👩🏻‍🦳 woman: light skin tone, white hair +#SUPPORT(prf) 1F469 1F3FC 200D 1F9B3 ; fully-qualified # 👩🏼‍🦳 woman: medium-light skin tone, white hair +#SUPPORT(prf) 1F469 1F3FD 200D 1F9B3 ; fully-qualified # 👩🏽‍🦳 woman: medium skin tone, white hair +#SUPPORT(prf) 1F469 1F3FE 200D 1F9B3 ; fully-qualified # 👩🏾‍🦳 woman: medium-dark skin tone, white hair +#SUPPORT(prf) 1F469 1F3FF 200D 1F9B3 ; fully-qualified # 👩🏿‍🦳 woman: dark skin tone, white hair +#SUPPORT(prf) 1F469 200D 1F9B2 ; fully-qualified # 👩‍🦲 woman: bald +#SUPPORT(prf) 1F469 1F3FB 200D 1F9B2 ; fully-qualified # 👩🏻‍🦲 woman: light skin tone, bald +#SUPPORT(prf) 1F469 1F3FC 200D 1F9B2 ; fully-qualified # 👩🏼‍🦲 woman: medium-light skin tone, bald +#SUPPORT(prf) 1F469 1F3FD 200D 1F9B2 ; fully-qualified # 👩🏽‍🦲 woman: medium skin tone, bald +#SUPPORT(prf) 1F469 1F3FE 200D 1F9B2 ; fully-qualified # 👩🏾‍🦲 woman: medium-dark skin tone, bald +#SUPPORT(prf) 1F469 1F3FF 200D 1F9B2 ; fully-qualified # 👩🏿‍🦲 woman: dark skin tone, bald +1F9D3 ; fully-qualified # 🧓 older person +1F9D3 1F3FB ; fully-qualified # 🧓🏻 older person: light skin tone +1F9D3 1F3FC ; fully-qualified # 🧓🏼 older person: medium-light skin tone +1F9D3 1F3FD ; fully-qualified # 🧓🏽 older person: medium skin tone +1F9D3 1F3FE ; fully-qualified # 🧓🏾 older person: medium-dark skin tone +1F9D3 1F3FF ; fully-qualified # 🧓🏿 older person: dark skin tone +1F474 ; fully-qualified # 👴 old man +1F474 1F3FB ; fully-qualified # 👴🏻 old man: light skin tone +1F474 1F3FC ; fully-qualified # 👴🏼 old man: medium-light skin tone +1F474 1F3FD ; fully-qualified # 👴🏽 old man: medium skin tone +1F474 1F3FE ; fully-qualified # 👴🏾 old man: medium-dark skin tone +1F474 1F3FF ; fully-qualified # 👴🏿 old man: dark skin tone +1F475 ; fully-qualified # 👵 old woman +1F475 1F3FB ; fully-qualified # 👵🏻 old woman: light skin tone +1F475 1F3FC ; fully-qualified # 👵🏼 old woman: medium-light skin tone +1F475 1F3FD ; fully-qualified # 👵🏽 old woman: medium skin tone +1F475 1F3FE ; fully-qualified # 👵🏾 old woman: medium-dark skin tone +1F475 1F3FF ; fully-qualified # 👵🏿 old woman: dark skin tone + +# subgroup: person-gesture +1F64D ; fully-qualified # 🙍 person frowning +1F64D 1F3FB ; fully-qualified # 🙍🏻 person frowning: light skin tone +1F64D 1F3FC ; fully-qualified # 🙍🏼 person frowning: medium-light skin tone +1F64D 1F3FD ; fully-qualified # 🙍🏽 person frowning: medium skin tone +1F64D 1F3FE ; fully-qualified # 🙍🏾 person frowning: medium-dark skin tone +1F64D 1F3FF ; fully-qualified # 🙍🏿 person frowning: dark skin tone +#SUPPORT(prf) 1F64D 200D 2642 FE0F ; fully-qualified # 🙍‍♂️ man frowning +#SUPPORT(prf) 1F64D 1F3FB 200D 2642 FE0F ; fully-qualified # 🙍🏻‍♂️ man frowning: light skin tone +#SUPPORT(prf) 1F64D 1F3FC 200D 2642 FE0F ; fully-qualified # 🙍🏼‍♂️ man frowning: medium-light skin tone +#SUPPORT(prf) 1F64D 1F3FD 200D 2642 FE0F ; fully-qualified # 🙍🏽‍♂️ man frowning: medium skin tone +#SUPPORT(prf) 1F64D 1F3FE 200D 2642 FE0F ; fully-qualified # 🙍🏾‍♂️ man frowning: medium-dark skin tone +#SUPPORT(prf) 1F64D 1F3FF 200D 2642 FE0F ; fully-qualified # 🙍🏿‍♂️ man frowning: dark skin tone +#SUPPORT(prf) 1F64D 200D 2640 FE0F ; fully-qualified # 🙍‍♀️ woman frowning +#SUPPORT(prf) 1F64D 1F3FB 200D 2640 FE0F ; fully-qualified # 🙍🏻‍♀️ woman frowning: light skin tone +#SUPPORT(prf) 1F64D 1F3FC 200D 2640 FE0F ; fully-qualified # 🙍🏼‍♀️ woman frowning: medium-light skin tone +#SUPPORT(prf) 1F64D 1F3FD 200D 2640 FE0F ; fully-qualified # 🙍🏽‍♀️ woman frowning: medium skin tone +#SUPPORT(prf) 1F64D 1F3FE 200D 2640 FE0F ; fully-qualified # 🙍🏾‍♀️ woman frowning: medium-dark skin tone +#SUPPORT(prf) 1F64D 1F3FF 200D 2640 FE0F ; fully-qualified # 🙍🏿‍♀️ woman frowning: dark skin tone +1F64E ; fully-qualified # 🙎 person pouting +1F64E 1F3FB ; fully-qualified # 🙎🏻 person pouting: light skin tone +1F64E 1F3FC ; fully-qualified # 🙎🏼 person pouting: medium-light skin tone +1F64E 1F3FD ; fully-qualified # 🙎🏽 person pouting: medium skin tone +1F64E 1F3FE ; fully-qualified # 🙎🏾 person pouting: medium-dark skin tone +1F64E 1F3FF ; fully-qualified # 🙎🏿 person pouting: dark skin tone +#SUPPORT(prf) 1F64E 200D 2642 FE0F ; fully-qualified # 🙎‍♂️ man pouting +#SUPPORT(prf) 1F64E 1F3FB 200D 2642 FE0F ; fully-qualified # 🙎🏻‍♂️ man pouting: light skin tone +#SUPPORT(prf) 1F64E 1F3FC 200D 2642 FE0F ; fully-qualified # 🙎🏼‍♂️ man pouting: medium-light skin tone +#SUPPORT(prf) 1F64E 1F3FD 200D 2642 FE0F ; fully-qualified # 🙎🏽‍♂️ man pouting: medium skin tone +#SUPPORT(prf) 1F64E 1F3FE 200D 2642 FE0F ; fully-qualified # 🙎🏾‍♂️ man pouting: medium-dark skin tone +#SUPPORT(prf) 1F64E 1F3FF 200D 2642 FE0F ; fully-qualified # 🙎🏿‍♂️ man pouting: dark skin tone +#SUPPORT(prf) 1F64E 200D 2640 FE0F ; fully-qualified # 🙎‍♀️ woman pouting +#SUPPORT(prf) 1F64E 1F3FB 200D 2640 FE0F ; fully-qualified # 🙎🏻‍♀️ woman pouting: light skin tone +#SUPPORT(prf) 1F64E 1F3FC 200D 2640 FE0F ; fully-qualified # 🙎🏼‍♀️ woman pouting: medium-light skin tone +#SUPPORT(prf) 1F64E 1F3FD 200D 2640 FE0F ; fully-qualified # 🙎🏽‍♀️ woman pouting: medium skin tone +#SUPPORT(prf) 1F64E 1F3FE 200D 2640 FE0F ; fully-qualified # 🙎🏾‍♀️ woman pouting: medium-dark skin tone +#SUPPORT(prf) 1F64E 1F3FF 200D 2640 FE0F ; fully-qualified # 🙎🏿‍♀️ woman pouting: dark skin tone +1F645 ; fully-qualified # 🙅 person gesturing NO +1F645 1F3FB ; fully-qualified # 🙅🏻 person gesturing NO: light skin tone +1F645 1F3FC ; fully-qualified # 🙅🏼 person gesturing NO: medium-light skin tone +1F645 1F3FD ; fully-qualified # 🙅🏽 person gesturing NO: medium skin tone +1F645 1F3FE ; fully-qualified # 🙅🏾 person gesturing NO: medium-dark skin tone +1F645 1F3FF ; fully-qualified # 🙅🏿 person gesturing NO: dark skin tone +#SUPPORT(prf) 1F645 200D 2642 FE0F ; fully-qualified # 🙅‍♂️ man gesturing NO +#SUPPORT(prf) 1F645 1F3FB 200D 2642 FE0F ; fully-qualified # 🙅🏻‍♂️ man gesturing NO: light skin tone +#SUPPORT(prf) 1F645 1F3FC 200D 2642 FE0F ; fully-qualified # 🙅🏼‍♂️ man gesturing NO: medium-light skin tone +#SUPPORT(prf) 1F645 1F3FD 200D 2642 FE0F ; fully-qualified # 🙅🏽‍♂️ man gesturing NO: medium skin tone +#SUPPORT(prf) 1F645 1F3FE 200D 2642 FE0F ; fully-qualified # 🙅🏾‍♂️ man gesturing NO: medium-dark skin tone +#SUPPORT(prf) 1F645 1F3FF 200D 2642 FE0F ; fully-qualified # 🙅🏿‍♂️ man gesturing NO: dark skin tone +#SUPPORT(prf) 1F645 200D 2640 FE0F ; fully-qualified # 🙅‍♀️ woman gesturing NO +#SUPPORT(prf) 1F645 1F3FB 200D 2640 FE0F ; fully-qualified # 🙅🏻‍♀️ woman gesturing NO: light skin tone +#SUPPORT(prf) 1F645 1F3FC 200D 2640 FE0F ; fully-qualified # 🙅🏼‍♀️ woman gesturing NO: medium-light skin tone +#SUPPORT(prf) 1F645 1F3FD 200D 2640 FE0F ; fully-qualified # 🙅🏽‍♀️ woman gesturing NO: medium skin tone +#SUPPORT(prf) 1F645 1F3FE 200D 2640 FE0F ; fully-qualified # 🙅🏾‍♀️ woman gesturing NO: medium-dark skin tone +#SUPPORT(prf) 1F645 1F3FF 200D 2640 FE0F ; fully-qualified # 🙅🏿‍♀️ woman gesturing NO: dark skin tone +1F646 ; fully-qualified # 🙆 person gesturing OK +1F646 1F3FB ; fully-qualified # 🙆🏻 person gesturing OK: light skin tone +1F646 1F3FC ; fully-qualified # 🙆🏼 person gesturing OK: medium-light skin tone +1F646 1F3FD ; fully-qualified # 🙆🏽 person gesturing OK: medium skin tone +1F646 1F3FE ; fully-qualified # 🙆🏾 person gesturing OK: medium-dark skin tone +1F646 1F3FF ; fully-qualified # 🙆🏿 person gesturing OK: dark skin tone +#SUPPORT(prf) 1F646 200D 2642 FE0F ; fully-qualified # 🙆‍♂️ man gesturing OK +#SUPPORT(prf) 1F646 1F3FB 200D 2642 FE0F ; fully-qualified # 🙆🏻‍♂️ man gesturing OK: light skin tone +#SUPPORT(prf) 1F646 1F3FC 200D 2642 FE0F ; fully-qualified # 🙆🏼‍♂️ man gesturing OK: medium-light skin tone +#SUPPORT(prf) 1F646 1F3FD 200D 2642 FE0F ; fully-qualified # 🙆🏽‍♂️ man gesturing OK: medium skin tone +#SUPPORT(prf) 1F646 1F3FE 200D 2642 FE0F ; fully-qualified # 🙆🏾‍♂️ man gesturing OK: medium-dark skin tone +#SUPPORT(prf) 1F646 1F3FF 200D 2642 FE0F ; fully-qualified # 🙆🏿‍♂️ man gesturing OK: dark skin tone +#SUPPORT(prf) 1F646 200D 2640 FE0F ; fully-qualified # 🙆‍♀️ woman gesturing OK +#SUPPORT(prf) 1F646 1F3FB 200D 2640 FE0F ; fully-qualified # 🙆🏻‍♀️ woman gesturing OK: light skin tone +#SUPPORT(prf) 1F646 1F3FC 200D 2640 FE0F ; fully-qualified # 🙆🏼‍♀️ woman gesturing OK: medium-light skin tone +#SUPPORT(prf) 1F646 1F3FD 200D 2640 FE0F ; fully-qualified # 🙆🏽‍♀️ woman gesturing OK: medium skin tone +#SUPPORT(prf) 1F646 1F3FE 200D 2640 FE0F ; fully-qualified # 🙆🏾‍♀️ woman gesturing OK: medium-dark skin tone +#SUPPORT(prf) 1F646 1F3FF 200D 2640 FE0F ; fully-qualified # 🙆🏿‍♀️ woman gesturing OK: dark skin tone +1F481 ; fully-qualified # 💁 person tipping hand +1F481 1F3FB ; fully-qualified # 💁🏻 person tipping hand: light skin tone +1F481 1F3FC ; fully-qualified # 💁🏼 person tipping hand: medium-light skin tone +1F481 1F3FD ; fully-qualified # 💁🏽 person tipping hand: medium skin tone +1F481 1F3FE ; fully-qualified # 💁🏾 person tipping hand: medium-dark skin tone +1F481 1F3FF ; fully-qualified # 💁🏿 person tipping hand: dark skin tone +#SUPPORT(prf) 1F481 200D 2642 FE0F ; fully-qualified # 💁‍♂️ man tipping hand +#SUPPORT(prf) 1F481 1F3FB 200D 2642 FE0F ; fully-qualified # 💁🏻‍♂️ man tipping hand: light skin tone +#SUPPORT(prf) 1F481 1F3FC 200D 2642 FE0F ; fully-qualified # 💁🏼‍♂️ man tipping hand: medium-light skin tone +#SUPPORT(prf) 1F481 1F3FD 200D 2642 FE0F ; fully-qualified # 💁🏽‍♂️ man tipping hand: medium skin tone +#SUPPORT(prf) 1F481 1F3FE 200D 2642 FE0F ; fully-qualified # 💁🏾‍♂️ man tipping hand: medium-dark skin tone +#SUPPORT(prf) 1F481 1F3FF 200D 2642 FE0F ; fully-qualified # 💁🏿‍♂️ man tipping hand: dark skin tone +#SUPPORT(prf) 1F481 200D 2640 FE0F ; fully-qualified # 💁‍♀️ woman tipping hand +#SUPPORT(prf) 1F481 1F3FB 200D 2640 FE0F ; fully-qualified # 💁🏻‍♀️ woman tipping hand: light skin tone +#SUPPORT(prf) 1F481 1F3FC 200D 2640 FE0F ; fully-qualified # 💁🏼‍♀️ woman tipping hand: medium-light skin tone +#SUPPORT(prf) 1F481 1F3FD 200D 2640 FE0F ; fully-qualified # 💁🏽‍♀️ woman tipping hand: medium skin tone +#SUPPORT(prf) 1F481 1F3FE 200D 2640 FE0F ; fully-qualified # 💁🏾‍♀️ woman tipping hand: medium-dark skin tone +#SUPPORT(prf) 1F481 1F3FF 200D 2640 FE0F ; fully-qualified # 💁🏿‍♀️ woman tipping hand: dark skin tone +1F64B ; fully-qualified # 🙋 person raising hand +1F64B 1F3FB ; fully-qualified # 🙋🏻 person raising hand: light skin tone +1F64B 1F3FC ; fully-qualified # 🙋🏼 person raising hand: medium-light skin tone +1F64B 1F3FD ; fully-qualified # 🙋🏽 person raising hand: medium skin tone +1F64B 1F3FE ; fully-qualified # 🙋🏾 person raising hand: medium-dark skin tone +1F64B 1F3FF ; fully-qualified # 🙋🏿 person raising hand: dark skin tone +#SUPPORT(prf) 1F64B 200D 2642 FE0F ; fully-qualified # 🙋‍♂️ man raising hand +#SUPPORT(prf) 1F64B 1F3FB 200D 2642 FE0F ; fully-qualified # 🙋🏻‍♂️ man raising hand: light skin tone +#SUPPORT(prf) 1F64B 1F3FC 200D 2642 FE0F ; fully-qualified # 🙋🏼‍♂️ man raising hand: medium-light skin tone +#SUPPORT(prf) 1F64B 1F3FD 200D 2642 FE0F ; fully-qualified # 🙋🏽‍♂️ man raising hand: medium skin tone +#SUPPORT(prf) 1F64B 1F3FE 200D 2642 FE0F ; fully-qualified # 🙋🏾‍♂️ man raising hand: medium-dark skin tone +#SUPPORT(prf) 1F64B 1F3FF 200D 2642 FE0F ; fully-qualified # 🙋🏿‍♂️ man raising hand: dark skin tone +#SUPPORT(prf) 1F64B 200D 2640 FE0F ; fully-qualified # 🙋‍♀️ woman raising hand +#SUPPORT(prf) 1F64B 1F3FB 200D 2640 FE0F ; fully-qualified # 🙋🏻‍♀️ woman raising hand: light skin tone +#SUPPORT(prf) 1F64B 1F3FC 200D 2640 FE0F ; fully-qualified # 🙋🏼‍♀️ woman raising hand: medium-light skin tone +#SUPPORT(prf) 1F64B 1F3FD 200D 2640 FE0F ; fully-qualified # 🙋🏽‍♀️ woman raising hand: medium skin tone +#SUPPORT(prf) 1F64B 1F3FE 200D 2640 FE0F ; fully-qualified # 🙋🏾‍♀️ woman raising hand: medium-dark skin tone +#SUPPORT(prf) 1F64B 1F3FF 200D 2640 FE0F ; fully-qualified # 🙋🏿‍♀️ woman raising hand: dark skin tone +#SUPPORT(prf) 1F9CF ; fully-qualified # 🧏 deaf person +#SUPPORT(prf) 1F9CF 1F3FB ; fully-qualified # 🧏🏻 deaf person: light skin tone +#SUPPORT(prf) 1F9CF 1F3FC ; fully-qualified # 🧏🏼 deaf person: medium-light skin tone +#SUPPORT(prf) 1F9CF 1F3FD ; fully-qualified # 🧏🏽 deaf person: medium skin tone +#SUPPORT(prf) 1F9CF 1F3FE ; fully-qualified # 🧏🏾 deaf person: medium-dark skin tone +#SUPPORT(prf) 1F9CF 1F3FF ; fully-qualified # 🧏🏿 deaf person: dark skin tone +#SUPPORT(prf) 1F9CF 200D 2642 FE0F ; fully-qualified # 🧏‍♂️ deaf man +#SUPPORT(prf) 1F9CF 1F3FB 200D 2642 FE0F ; fully-qualified # 🧏🏻‍♂️ deaf man: light skin tone +#SUPPORT(prf) 1F9CF 1F3FC 200D 2642 FE0F ; fully-qualified # 🧏🏼‍♂️ deaf man: medium-light skin tone +#SUPPORT(prf) 1F9CF 1F3FD 200D 2642 FE0F ; fully-qualified # 🧏🏽‍♂️ deaf man: medium skin tone +#SUPPORT(prf) 1F9CF 1F3FE 200D 2642 FE0F ; fully-qualified # 🧏🏾‍♂️ deaf man: medium-dark skin tone +#SUPPORT(prf) 1F9CF 1F3FF 200D 2642 FE0F ; fully-qualified # 🧏🏿‍♂️ deaf man: dark skin tone +#SUPPORT(prf) 1F9CF 200D 2640 FE0F ; fully-qualified # 🧏‍♀️ deaf woman +#SUPPORT(prf) 1F9CF 1F3FB 200D 2640 FE0F ; fully-qualified # 🧏🏻‍♀️ deaf woman: light skin tone +#SUPPORT(prf) 1F9CF 1F3FC 200D 2640 FE0F ; fully-qualified # 🧏🏼‍♀️ deaf woman: medium-light skin tone +#SUPPORT(prf) 1F9CF 1F3FD 200D 2640 FE0F ; fully-qualified # 🧏🏽‍♀️ deaf woman: medium skin tone +#SUPPORT(prf) 1F9CF 1F3FE 200D 2640 FE0F ; fully-qualified # 🧏🏾‍♀️ deaf woman: medium-dark skin tone +#SUPPORT(prf) 1F9CF 1F3FF 200D 2640 FE0F ; fully-qualified # 🧏🏿‍♀️ deaf woman: dark skin tone +1F647 ; fully-qualified # 🙇 person bowing +1F647 1F3FB ; fully-qualified # 🙇🏻 person bowing: light skin tone +1F647 1F3FC ; fully-qualified # 🙇🏼 person bowing: medium-light skin tone +1F647 1F3FD ; fully-qualified # 🙇🏽 person bowing: medium skin tone +1F647 1F3FE ; fully-qualified # 🙇🏾 person bowing: medium-dark skin tone +1F647 1F3FF ; fully-qualified # 🙇🏿 person bowing: dark skin tone +#SUPPORT(prf) 1F647 200D 2642 FE0F ; fully-qualified # 🙇‍♂️ man bowing +#SUPPORT(prf) 1F647 1F3FB 200D 2642 FE0F ; fully-qualified # 🙇🏻‍♂️ man bowing: light skin tone +#SUPPORT(prf) 1F647 1F3FC 200D 2642 FE0F ; fully-qualified # 🙇🏼‍♂️ man bowing: medium-light skin tone +#SUPPORT(prf) 1F647 1F3FD 200D 2642 FE0F ; fully-qualified # 🙇🏽‍♂️ man bowing: medium skin tone +#SUPPORT(prf) 1F647 1F3FE 200D 2642 FE0F ; fully-qualified # 🙇🏾‍♂️ man bowing: medium-dark skin tone +#SUPPORT(prf) 1F647 1F3FF 200D 2642 FE0F ; fully-qualified # 🙇🏿‍♂️ man bowing: dark skin tone +#SUPPORT(prf) 1F647 200D 2640 FE0F ; fully-qualified # 🙇‍♀️ woman bowing +#SUPPORT(prf) 1F647 1F3FB 200D 2640 FE0F ; fully-qualified # 🙇🏻‍♀️ woman bowing: light skin tone +#SUPPORT(prf) 1F647 1F3FC 200D 2640 FE0F ; fully-qualified # 🙇🏼‍♀️ woman bowing: medium-light skin tone +#SUPPORT(prf) 1F647 1F3FD 200D 2640 FE0F ; fully-qualified # 🙇🏽‍♀️ woman bowing: medium skin tone +#SUPPORT(prf) 1F647 1F3FE 200D 2640 FE0F ; fully-qualified # 🙇🏾‍♀️ woman bowing: medium-dark skin tone +#SUPPORT(prf) 1F647 1F3FF 200D 2640 FE0F ; fully-qualified # 🙇🏿‍♀️ woman bowing: dark skin tone +1F926 ; fully-qualified # 🤦 person facepalming +1F926 1F3FB ; fully-qualified # 🤦🏻 person facepalming: light skin tone +1F926 1F3FC ; fully-qualified # 🤦🏼 person facepalming: medium-light skin tone +1F926 1F3FD ; fully-qualified # 🤦🏽 person facepalming: medium skin tone +1F926 1F3FE ; fully-qualified # 🤦🏾 person facepalming: medium-dark skin tone +1F926 1F3FF ; fully-qualified # 🤦🏿 person facepalming: dark skin tone +#SUPPORT(prf) 1F926 200D 2642 FE0F ; fully-qualified # 🤦‍♂️ man facepalming +#SUPPORT(prf) 1F926 1F3FB 200D 2642 FE0F ; fully-qualified # 🤦🏻‍♂️ man facepalming: light skin tone +#SUPPORT(prf) 1F926 1F3FC 200D 2642 FE0F ; fully-qualified # 🤦🏼‍♂️ man facepalming: medium-light skin tone +#SUPPORT(prf) 1F926 1F3FD 200D 2642 FE0F ; fully-qualified # 🤦🏽‍♂️ man facepalming: medium skin tone +#SUPPORT(prf) 1F926 1F3FE 200D 2642 FE0F ; fully-qualified # 🤦🏾‍♂️ man facepalming: medium-dark skin tone +#SUPPORT(prf) 1F926 1F3FF 200D 2642 FE0F ; fully-qualified # 🤦🏿‍♂️ man facepalming: dark skin tone +#SUPPORT(prf) 1F926 200D 2640 FE0F ; fully-qualified # 🤦‍♀️ woman facepalming +#SUPPORT(prf) 1F926 1F3FB 200D 2640 FE0F ; fully-qualified # 🤦🏻‍♀️ woman facepalming: light skin tone +#SUPPORT(prf) 1F926 1F3FC 200D 2640 FE0F ; fully-qualified # 🤦🏼‍♀️ woman facepalming: medium-light skin tone +#SUPPORT(prf) 1F926 1F3FD 200D 2640 FE0F ; fully-qualified # 🤦🏽‍♀️ woman facepalming: medium skin tone +#SUPPORT(prf) 1F926 1F3FE 200D 2640 FE0F ; fully-qualified # 🤦🏾‍♀️ woman facepalming: medium-dark skin tone +#SUPPORT(prf) 1F926 1F3FF 200D 2640 FE0F ; fully-qualified # 🤦🏿‍♀️ woman facepalming: dark skin tone +1F937 ; fully-qualified # 🤷 person shrugging +1F937 1F3FB ; fully-qualified # 🤷🏻 person shrugging: light skin tone +1F937 1F3FC ; fully-qualified # 🤷🏼 person shrugging: medium-light skin tone +1F937 1F3FD ; fully-qualified # 🤷🏽 person shrugging: medium skin tone +1F937 1F3FE ; fully-qualified # 🤷🏾 person shrugging: medium-dark skin tone +1F937 1F3FF ; fully-qualified # 🤷🏿 person shrugging: dark skin tone +#SUPPORT(prf) 1F937 200D 2642 FE0F ; fully-qualified # 🤷‍♂️ man shrugging +#SUPPORT(prf) 1F937 1F3FB 200D 2642 FE0F ; fully-qualified # 🤷🏻‍♂️ man shrugging: light skin tone +#SUPPORT(prf) 1F937 1F3FC 200D 2642 FE0F ; fully-qualified # 🤷🏼‍♂️ man shrugging: medium-light skin tone +#SUPPORT(prf) 1F937 1F3FD 200D 2642 FE0F ; fully-qualified # 🤷🏽‍♂️ man shrugging: medium skin tone +#SUPPORT(prf) 1F937 1F3FE 200D 2642 FE0F ; fully-qualified # 🤷🏾‍♂️ man shrugging: medium-dark skin tone +#SUPPORT(prf) 1F937 1F3FF 200D 2642 FE0F ; fully-qualified # 🤷🏿‍♂️ man shrugging: dark skin tone +#SUPPORT(prf) 1F937 200D 2640 FE0F ; fully-qualified # 🤷‍♀️ woman shrugging +#SUPPORT(prf) 1F937 1F3FB 200D 2640 FE0F ; fully-qualified # 🤷🏻‍♀️ woman shrugging: light skin tone +#SUPPORT(prf) 1F937 1F3FC 200D 2640 FE0F ; fully-qualified # 🤷🏼‍♀️ woman shrugging: medium-light skin tone +#SUPPORT(prf) 1F937 1F3FD 200D 2640 FE0F ; fully-qualified # 🤷🏽‍♀️ woman shrugging: medium skin tone +#SUPPORT(prf) 1F937 1F3FE 200D 2640 FE0F ; fully-qualified # 🤷🏾‍♀️ woman shrugging: medium-dark skin tone +#SUPPORT(prf) 1F937 1F3FF 200D 2640 FE0F ; fully-qualified # 🤷🏿‍♀️ woman shrugging: dark skin tone + +# subgroup: person-role +1F468 200D 2695 FE0F ; fully-qualified # 👨‍⚕️ man health worker +1F468 1F3FB 200D 2695 FE0F ; fully-qualified # 👨🏻‍⚕️ man health worker: light skin tone +1F468 1F3FC 200D 2695 FE0F ; fully-qualified # 👨🏼‍⚕️ man health worker: medium-light skin tone +1F468 1F3FD 200D 2695 FE0F ; fully-qualified # 👨🏽‍⚕️ man health worker: medium skin tone +1F468 1F3FE 200D 2695 FE0F ; fully-qualified # 👨🏾‍⚕️ man health worker: medium-dark skin tone +1F468 1F3FF 200D 2695 FE0F ; fully-qualified # 👨🏿‍⚕️ man health worker: dark skin tone +1F469 200D 2695 FE0F ; fully-qualified # 👩‍⚕️ woman health worker +1F469 1F3FB 200D 2695 FE0F ; fully-qualified # 👩🏻‍⚕️ woman health worker: light skin tone +1F469 1F3FC 200D 2695 FE0F ; fully-qualified # 👩🏼‍⚕️ woman health worker: medium-light skin tone +1F469 1F3FD 200D 2695 FE0F ; fully-qualified # 👩🏽‍⚕️ woman health worker: medium skin tone +1F469 1F3FE 200D 2695 FE0F ; fully-qualified # 👩🏾‍⚕️ woman health worker: medium-dark skin tone +1F469 1F3FF 200D 2695 FE0F ; fully-qualified # 👩🏿‍⚕️ woman health worker: dark skin tone +1F468 200D 1F393 ; fully-qualified # 👨‍🎓 man student +1F468 1F3FB 200D 1F393 ; fully-qualified # 👨🏻‍🎓 man student: light skin tone +1F468 1F3FC 200D 1F393 ; fully-qualified # 👨🏼‍🎓 man student: medium-light skin tone +1F468 1F3FD 200D 1F393 ; fully-qualified # 👨🏽‍🎓 man student: medium skin tone +1F468 1F3FE 200D 1F393 ; fully-qualified # 👨🏾‍🎓 man student: medium-dark skin tone +1F468 1F3FF 200D 1F393 ; fully-qualified # 👨🏿‍🎓 man student: dark skin tone +1F469 200D 1F393 ; fully-qualified # 👩‍🎓 woman student +1F469 1F3FB 200D 1F393 ; fully-qualified # 👩🏻‍🎓 woman student: light skin tone +1F469 1F3FC 200D 1F393 ; fully-qualified # 👩🏼‍🎓 woman student: medium-light skin tone +1F469 1F3FD 200D 1F393 ; fully-qualified # 👩🏽‍🎓 woman student: medium skin tone +1F469 1F3FE 200D 1F393 ; fully-qualified # 👩🏾‍🎓 woman student: medium-dark skin tone +1F469 1F3FF 200D 1F393 ; fully-qualified # 👩🏿‍🎓 woman student: dark skin tone +1F468 200D 1F3EB ; fully-qualified # 👨‍🏫 man teacher +1F468 1F3FB 200D 1F3EB ; fully-qualified # 👨🏻‍🏫 man teacher: light skin tone +1F468 1F3FC 200D 1F3EB ; fully-qualified # 👨🏼‍🏫 man teacher: medium-light skin tone +1F468 1F3FD 200D 1F3EB ; fully-qualified # 👨🏽‍🏫 man teacher: medium skin tone +1F468 1F3FE 200D 1F3EB ; fully-qualified # 👨🏾‍🏫 man teacher: medium-dark skin tone +1F468 1F3FF 200D 1F3EB ; fully-qualified # 👨🏿‍🏫 man teacher: dark skin tone +1F469 200D 1F3EB ; fully-qualified # 👩‍🏫 woman teacher +1F469 1F3FB 200D 1F3EB ; fully-qualified # 👩🏻‍🏫 woman teacher: light skin tone +1F469 1F3FC 200D 1F3EB ; fully-qualified # 👩🏼‍🏫 woman teacher: medium-light skin tone +1F469 1F3FD 200D 1F3EB ; fully-qualified # 👩🏽‍🏫 woman teacher: medium skin tone +1F469 1F3FE 200D 1F3EB ; fully-qualified # 👩🏾‍🏫 woman teacher: medium-dark skin tone +1F469 1F3FF 200D 1F3EB ; fully-qualified # 👩🏿‍🏫 woman teacher: dark skin tone +1F468 200D 2696 FE0F ; fully-qualified # 👨‍⚖️ man judge +1F468 1F3FB 200D 2696 FE0F ; fully-qualified # 👨🏻‍⚖️ man judge: light skin tone +1F468 1F3FC 200D 2696 FE0F ; fully-qualified # 👨🏼‍⚖️ man judge: medium-light skin tone +1F468 1F3FD 200D 2696 FE0F ; fully-qualified # 👨🏽‍⚖️ man judge: medium skin tone +1F468 1F3FE 200D 2696 FE0F ; fully-qualified # 👨🏾‍⚖️ man judge: medium-dark skin tone +1F468 1F3FF 200D 2696 FE0F ; fully-qualified # 👨🏿‍⚖️ man judge: dark skin tone +1F469 200D 2696 FE0F ; fully-qualified # 👩‍⚖️ woman judge +1F469 1F3FB 200D 2696 FE0F ; fully-qualified # 👩🏻‍⚖️ woman judge: light skin tone +1F469 1F3FC 200D 2696 FE0F ; fully-qualified # 👩🏼‍⚖️ woman judge: medium-light skin tone +1F469 1F3FD 200D 2696 FE0F ; fully-qualified # 👩🏽‍⚖️ woman judge: medium skin tone +1F469 1F3FE 200D 2696 FE0F ; fully-qualified # 👩🏾‍⚖️ woman judge: medium-dark skin tone +1F469 1F3FF 200D 2696 FE0F ; fully-qualified # 👩🏿‍⚖️ woman judge: dark skin tone +1F468 200D 1F33E ; fully-qualified # 👨‍🌾 man farmer +1F468 1F3FB 200D 1F33E ; fully-qualified # 👨🏻‍🌾 man farmer: light skin tone +1F468 1F3FC 200D 1F33E ; fully-qualified # 👨🏼‍🌾 man farmer: medium-light skin tone +1F468 1F3FD 200D 1F33E ; fully-qualified # 👨🏽‍🌾 man farmer: medium skin tone +1F468 1F3FE 200D 1F33E ; fully-qualified # 👨🏾‍🌾 man farmer: medium-dark skin tone +1F468 1F3FF 200D 1F33E ; fully-qualified # 👨🏿‍🌾 man farmer: dark skin tone +1F469 200D 1F33E ; fully-qualified # 👩‍🌾 woman farmer +1F469 1F3FB 200D 1F33E ; fully-qualified # 👩🏻‍🌾 woman farmer: light skin tone +1F469 1F3FC 200D 1F33E ; fully-qualified # 👩🏼‍🌾 woman farmer: medium-light skin tone +1F469 1F3FD 200D 1F33E ; fully-qualified # 👩🏽‍🌾 woman farmer: medium skin tone +1F469 1F3FE 200D 1F33E ; fully-qualified # 👩🏾‍🌾 woman farmer: medium-dark skin tone +1F469 1F3FF 200D 1F33E ; fully-qualified # 👩🏿‍🌾 woman farmer: dark skin tone +1F468 200D 1F373 ; fully-qualified # 👨‍🍳 man cook +1F468 1F3FB 200D 1F373 ; fully-qualified # 👨🏻‍🍳 man cook: light skin tone +1F468 1F3FC 200D 1F373 ; fully-qualified # 👨🏼‍🍳 man cook: medium-light skin tone +1F468 1F3FD 200D 1F373 ; fully-qualified # 👨🏽‍🍳 man cook: medium skin tone +1F468 1F3FE 200D 1F373 ; fully-qualified # 👨🏾‍🍳 man cook: medium-dark skin tone +1F468 1F3FF 200D 1F373 ; fully-qualified # 👨🏿‍🍳 man cook: dark skin tone +1F469 200D 1F373 ; fully-qualified # 👩‍🍳 woman cook +1F469 1F3FB 200D 1F373 ; fully-qualified # 👩🏻‍🍳 woman cook: light skin tone +1F469 1F3FC 200D 1F373 ; fully-qualified # 👩🏼‍🍳 woman cook: medium-light skin tone +1F469 1F3FD 200D 1F373 ; fully-qualified # 👩🏽‍🍳 woman cook: medium skin tone +1F469 1F3FE 200D 1F373 ; fully-qualified # 👩🏾‍🍳 woman cook: medium-dark skin tone +1F469 1F3FF 200D 1F373 ; fully-qualified # 👩🏿‍🍳 woman cook: dark skin tone +1F468 200D 1F527 ; fully-qualified # 👨‍🔧 man mechanic +1F468 1F3FB 200D 1F527 ; fully-qualified # 👨🏻‍🔧 man mechanic: light skin tone +1F468 1F3FC 200D 1F527 ; fully-qualified # 👨🏼‍🔧 man mechanic: medium-light skin tone +1F468 1F3FD 200D 1F527 ; fully-qualified # 👨🏽‍🔧 man mechanic: medium skin tone +1F468 1F3FE 200D 1F527 ; fully-qualified # 👨🏾‍🔧 man mechanic: medium-dark skin tone +1F468 1F3FF 200D 1F527 ; fully-qualified # 👨🏿‍🔧 man mechanic: dark skin tone +1F469 200D 1F527 ; fully-qualified # 👩‍🔧 woman mechanic +1F469 1F3FB 200D 1F527 ; fully-qualified # 👩🏻‍🔧 woman mechanic: light skin tone +1F469 1F3FC 200D 1F527 ; fully-qualified # 👩🏼‍🔧 woman mechanic: medium-light skin tone +1F469 1F3FD 200D 1F527 ; fully-qualified # 👩🏽‍🔧 woman mechanic: medium skin tone +1F469 1F3FE 200D 1F527 ; fully-qualified # 👩🏾‍🔧 woman mechanic: medium-dark skin tone +1F469 1F3FF 200D 1F527 ; fully-qualified # 👩🏿‍🔧 woman mechanic: dark skin tone +1F468 200D 1F3ED ; fully-qualified # 👨‍🏭 man factory worker +1F468 1F3FB 200D 1F3ED ; fully-qualified # 👨🏻‍🏭 man factory worker: light skin tone +1F468 1F3FC 200D 1F3ED ; fully-qualified # 👨🏼‍🏭 man factory worker: medium-light skin tone +1F468 1F3FD 200D 1F3ED ; fully-qualified # 👨🏽‍🏭 man factory worker: medium skin tone +1F468 1F3FE 200D 1F3ED ; fully-qualified # 👨🏾‍🏭 man factory worker: medium-dark skin tone +1F468 1F3FF 200D 1F3ED ; fully-qualified # 👨🏿‍🏭 man factory worker: dark skin tone +1F469 200D 1F3ED ; fully-qualified # 👩‍🏭 woman factory worker +1F469 1F3FB 200D 1F3ED ; fully-qualified # 👩🏻‍🏭 woman factory worker: light skin tone +1F469 1F3FC 200D 1F3ED ; fully-qualified # 👩🏼‍🏭 woman factory worker: medium-light skin tone +1F469 1F3FD 200D 1F3ED ; fully-qualified # 👩🏽‍🏭 woman factory worker: medium skin tone +1F469 1F3FE 200D 1F3ED ; fully-qualified # 👩🏾‍🏭 woman factory worker: medium-dark skin tone +1F469 1F3FF 200D 1F3ED ; fully-qualified # 👩🏿‍🏭 woman factory worker: dark skin tone +1F468 200D 1F4BC ; fully-qualified # 👨‍💼 man office worker +1F468 1F3FB 200D 1F4BC ; fully-qualified # 👨🏻‍💼 man office worker: light skin tone +1F468 1F3FC 200D 1F4BC ; fully-qualified # 👨🏼‍💼 man office worker: medium-light skin tone +1F468 1F3FD 200D 1F4BC ; fully-qualified # 👨🏽‍💼 man office worker: medium skin tone +1F468 1F3FE 200D 1F4BC ; fully-qualified # 👨🏾‍💼 man office worker: medium-dark skin tone +1F468 1F3FF 200D 1F4BC ; fully-qualified # 👨🏿‍💼 man office worker: dark skin tone +1F469 200D 1F4BC ; fully-qualified # 👩‍💼 woman office worker +1F469 1F3FB 200D 1F4BC ; fully-qualified # 👩🏻‍💼 woman office worker: light skin tone +1F469 1F3FC 200D 1F4BC ; fully-qualified # 👩🏼‍💼 woman office worker: medium-light skin tone +1F469 1F3FD 200D 1F4BC ; fully-qualified # 👩🏽‍💼 woman office worker: medium skin tone +1F469 1F3FE 200D 1F4BC ; fully-qualified # 👩🏾‍💼 woman office worker: medium-dark skin tone +1F469 1F3FF 200D 1F4BC ; fully-qualified # 👩🏿‍💼 woman office worker: dark skin tone +1F468 200D 1F52C ; fully-qualified # 👨‍🔬 man scientist +1F468 1F3FB 200D 1F52C ; fully-qualified # 👨🏻‍🔬 man scientist: light skin tone +1F468 1F3FC 200D 1F52C ; fully-qualified # 👨🏼‍🔬 man scientist: medium-light skin tone +1F468 1F3FD 200D 1F52C ; fully-qualified # 👨🏽‍🔬 man scientist: medium skin tone +1F468 1F3FE 200D 1F52C ; fully-qualified # 👨🏾‍🔬 man scientist: medium-dark skin tone +1F468 1F3FF 200D 1F52C ; fully-qualified # 👨🏿‍🔬 man scientist: dark skin tone +1F469 200D 1F52C ; fully-qualified # 👩‍🔬 woman scientist +1F469 1F3FB 200D 1F52C ; fully-qualified # 👩🏻‍🔬 woman scientist: light skin tone +1F469 1F3FC 200D 1F52C ; fully-qualified # 👩🏼‍🔬 woman scientist: medium-light skin tone +1F469 1F3FD 200D 1F52C ; fully-qualified # 👩🏽‍🔬 woman scientist: medium skin tone +1F469 1F3FE 200D 1F52C ; fully-qualified # 👩🏾‍🔬 woman scientist: medium-dark skin tone +1F469 1F3FF 200D 1F52C ; fully-qualified # 👩🏿‍🔬 woman scientist: dark skin tone +1F468 200D 1F4BB ; fully-qualified # 👨‍💻 man technologist +1F468 1F3FB 200D 1F4BB ; fully-qualified # 👨🏻‍💻 man technologist: light skin tone +1F468 1F3FC 200D 1F4BB ; fully-qualified # 👨🏼‍💻 man technologist: medium-light skin tone +1F468 1F3FD 200D 1F4BB ; fully-qualified # 👨🏽‍💻 man technologist: medium skin tone +1F468 1F3FE 200D 1F4BB ; fully-qualified # 👨🏾‍💻 man technologist: medium-dark skin tone +1F468 1F3FF 200D 1F4BB ; fully-qualified # 👨🏿‍💻 man technologist: dark skin tone +1F469 200D 1F4BB ; fully-qualified # 👩‍💻 woman technologist +1F469 1F3FB 200D 1F4BB ; fully-qualified # 👩🏻‍💻 woman technologist: light skin tone +1F469 1F3FC 200D 1F4BB ; fully-qualified # 👩🏼‍💻 woman technologist: medium-light skin tone +1F469 1F3FD 200D 1F4BB ; fully-qualified # 👩🏽‍💻 woman technologist: medium skin tone +1F469 1F3FE 200D 1F4BB ; fully-qualified # 👩🏾‍💻 woman technologist: medium-dark skin tone +1F469 1F3FF 200D 1F4BB ; fully-qualified # 👩🏿‍💻 woman technologist: dark skin tone +1F468 200D 1F3A4 ; fully-qualified # 👨‍🎤 man singer +1F468 1F3FB 200D 1F3A4 ; fully-qualified # 👨🏻‍🎤 man singer: light skin tone +1F468 1F3FC 200D 1F3A4 ; fully-qualified # 👨🏼‍🎤 man singer: medium-light skin tone +1F468 1F3FD 200D 1F3A4 ; fully-qualified # 👨🏽‍🎤 man singer: medium skin tone +1F468 1F3FE 200D 1F3A4 ; fully-qualified # 👨🏾‍🎤 man singer: medium-dark skin tone +1F468 1F3FF 200D 1F3A4 ; fully-qualified # 👨🏿‍🎤 man singer: dark skin tone +1F469 200D 1F3A4 ; fully-qualified # 👩‍🎤 woman singer +1F469 1F3FB 200D 1F3A4 ; fully-qualified # 👩🏻‍🎤 woman singer: light skin tone +1F469 1F3FC 200D 1F3A4 ; fully-qualified # 👩🏼‍🎤 woman singer: medium-light skin tone +1F469 1F3FD 200D 1F3A4 ; fully-qualified # 👩🏽‍🎤 woman singer: medium skin tone +1F469 1F3FE 200D 1F3A4 ; fully-qualified # 👩🏾‍🎤 woman singer: medium-dark skin tone +1F469 1F3FF 200D 1F3A4 ; fully-qualified # 👩🏿‍🎤 woman singer: dark skin tone +1F468 200D 1F3A8 ; fully-qualified # 👨‍🎨 man artist +1F468 1F3FB 200D 1F3A8 ; fully-qualified # 👨🏻‍🎨 man artist: light skin tone +1F468 1F3FC 200D 1F3A8 ; fully-qualified # 👨🏼‍🎨 man artist: medium-light skin tone +1F468 1F3FD 200D 1F3A8 ; fully-qualified # 👨🏽‍🎨 man artist: medium skin tone +1F468 1F3FE 200D 1F3A8 ; fully-qualified # 👨🏾‍🎨 man artist: medium-dark skin tone +1F468 1F3FF 200D 1F3A8 ; fully-qualified # 👨🏿‍🎨 man artist: dark skin tone +1F469 200D 1F3A8 ; fully-qualified # 👩‍🎨 woman artist +1F469 1F3FB 200D 1F3A8 ; fully-qualified # 👩🏻‍🎨 woman artist: light skin tone +1F469 1F3FC 200D 1F3A8 ; fully-qualified # 👩🏼‍🎨 woman artist: medium-light skin tone +1F469 1F3FD 200D 1F3A8 ; fully-qualified # 👩🏽‍🎨 woman artist: medium skin tone +1F469 1F3FE 200D 1F3A8 ; fully-qualified # 👩🏾‍🎨 woman artist: medium-dark skin tone +1F469 1F3FF 200D 1F3A8 ; fully-qualified # 👩🏿‍🎨 woman artist: dark skin tone +1F468 200D 2708 FE0F ; fully-qualified # 👨‍✈️ man pilot +1F468 1F3FB 200D 2708 FE0F ; fully-qualified # 👨🏻‍✈️ man pilot: light skin tone +1F468 1F3FC 200D 2708 FE0F ; fully-qualified # 👨🏼‍✈️ man pilot: medium-light skin tone +1F468 1F3FD 200D 2708 FE0F ; fully-qualified # 👨🏽‍✈️ man pilot: medium skin tone +1F468 1F3FE 200D 2708 FE0F ; fully-qualified # 👨🏾‍✈️ man pilot: medium-dark skin tone +1F468 1F3FF 200D 2708 FE0F ; fully-qualified # 👨🏿‍✈️ man pilot: dark skin tone +1F469 200D 2708 FE0F ; fully-qualified # 👩‍✈️ woman pilot +1F469 1F3FB 200D 2708 FE0F ; fully-qualified # 👩🏻‍✈️ woman pilot: light skin tone +1F469 1F3FC 200D 2708 FE0F ; fully-qualified # 👩🏼‍✈️ woman pilot: medium-light skin tone +1F469 1F3FD 200D 2708 FE0F ; fully-qualified # 👩🏽‍✈️ woman pilot: medium skin tone +1F469 1F3FE 200D 2708 FE0F ; fully-qualified # 👩🏾‍✈️ woman pilot: medium-dark skin tone +1F469 1F3FF 200D 2708 FE0F ; fully-qualified # 👩🏿‍✈️ woman pilot: dark skin tone +1F468 200D 1F680 ; fully-qualified # 👨‍🚀 man astronaut +1F468 1F3FB 200D 1F680 ; fully-qualified # 👨🏻‍🚀 man astronaut: light skin tone +1F468 1F3FC 200D 1F680 ; fully-qualified # 👨🏼‍🚀 man astronaut: medium-light skin tone +1F468 1F3FD 200D 1F680 ; fully-qualified # 👨🏽‍🚀 man astronaut: medium skin tone +1F468 1F3FE 200D 1F680 ; fully-qualified # 👨🏾‍🚀 man astronaut: medium-dark skin tone +1F468 1F3FF 200D 1F680 ; fully-qualified # 👨🏿‍🚀 man astronaut: dark skin tone +1F469 200D 1F680 ; fully-qualified # 👩‍🚀 woman astronaut +1F469 1F3FB 200D 1F680 ; fully-qualified # 👩🏻‍🚀 woman astronaut: light skin tone +1F469 1F3FC 200D 1F680 ; fully-qualified # 👩🏼‍🚀 woman astronaut: medium-light skin tone +1F469 1F3FD 200D 1F680 ; fully-qualified # 👩🏽‍🚀 woman astronaut: medium skin tone +1F469 1F3FE 200D 1F680 ; fully-qualified # 👩🏾‍🚀 woman astronaut: medium-dark skin tone +1F469 1F3FF 200D 1F680 ; fully-qualified # 👩🏿‍🚀 woman astronaut: dark skin tone +1F468 200D 1F692 ; fully-qualified # 👨‍🚒 man firefighter +1F468 1F3FB 200D 1F692 ; fully-qualified # 👨🏻‍🚒 man firefighter: light skin tone +1F468 1F3FC 200D 1F692 ; fully-qualified # 👨🏼‍🚒 man firefighter: medium-light skin tone +1F468 1F3FD 200D 1F692 ; fully-qualified # 👨🏽‍🚒 man firefighter: medium skin tone +1F468 1F3FE 200D 1F692 ; fully-qualified # 👨🏾‍🚒 man firefighter: medium-dark skin tone +1F468 1F3FF 200D 1F692 ; fully-qualified # 👨🏿‍🚒 man firefighter: dark skin tone +1F469 200D 1F692 ; fully-qualified # 👩‍🚒 woman firefighter +1F469 1F3FB 200D 1F692 ; fully-qualified # 👩🏻‍🚒 woman firefighter: light skin tone +1F469 1F3FC 200D 1F692 ; fully-qualified # 👩🏼‍🚒 woman firefighter: medium-light skin tone +1F469 1F3FD 200D 1F692 ; fully-qualified # 👩🏽‍🚒 woman firefighter: medium skin tone +1F469 1F3FE 200D 1F692 ; fully-qualified # 👩🏾‍🚒 woman firefighter: medium-dark skin tone +1F469 1F3FF 200D 1F692 ; fully-qualified # 👩🏿‍🚒 woman firefighter: dark skin tone +1F46E ; fully-qualified # 👮 police officer +1F46E 1F3FB ; fully-qualified # 👮🏻 police officer: light skin tone +1F46E 1F3FC ; fully-qualified # 👮🏼 police officer: medium-light skin tone +1F46E 1F3FD ; fully-qualified # 👮🏽 police officer: medium skin tone +1F46E 1F3FE ; fully-qualified # 👮🏾 police officer: medium-dark skin tone +1F46E 1F3FF ; fully-qualified # 👮🏿 police officer: dark skin tone +#SUPPORT(prf) 1F46E 200D 2642 FE0F ; fully-qualified # 👮‍♂️ man police officer +#SUPPORT(prf) 1F46E 1F3FB 200D 2642 FE0F ; fully-qualified # 👮🏻‍♂️ man police officer: light skin tone +#SUPPORT(prf) 1F46E 1F3FC 200D 2642 FE0F ; fully-qualified # 👮🏼‍♂️ man police officer: medium-light skin tone +#SUPPORT(prf) 1F46E 1F3FD 200D 2642 FE0F ; fully-qualified # 👮🏽‍♂️ man police officer: medium skin tone +#SUPPORT(prf) 1F46E 1F3FE 200D 2642 FE0F ; fully-qualified # 👮🏾‍♂️ man police officer: medium-dark skin tone +#SUPPORT(prf) 1F46E 1F3FF 200D 2642 FE0F ; fully-qualified # 👮🏿‍♂️ man police officer: dark skin tone +#SUPPORT(prf) 1F46E 200D 2640 FE0F ; fully-qualified # 👮‍♀️ woman police officer +#SUPPORT(prf) 1F46E 1F3FB 200D 2640 FE0F ; fully-qualified # 👮🏻‍♀️ woman police officer: light skin tone +#SUPPORT(prf) 1F46E 1F3FC 200D 2640 FE0F ; fully-qualified # 👮🏼‍♀️ woman police officer: medium-light skin tone +#SUPPORT(prf) 1F46E 1F3FD 200D 2640 FE0F ; fully-qualified # 👮🏽‍♀️ woman police officer: medium skin tone +#SUPPORT(prf) 1F46E 1F3FE 200D 2640 FE0F ; fully-qualified # 👮🏾‍♀️ woman police officer: medium-dark skin tone +#SUPPORT(prf) 1F46E 1F3FF 200D 2640 FE0F ; fully-qualified # 👮🏿‍♀️ woman police officer: dark skin tone +1F575 FE0F ; fully-qualified # 🕵️ detective +1F575 1F3FB ; fully-qualified # 🕵🏻 detective: light skin tone +1F575 1F3FC ; fully-qualified # 🕵🏼 detective: medium-light skin tone +1F575 1F3FD ; fully-qualified # 🕵🏽 detective: medium skin tone +1F575 1F3FE ; fully-qualified # 🕵🏾 detective: medium-dark skin tone +1F575 1F3FF ; fully-qualified # 🕵🏿 detective: dark skin tone +#SUPPORT(prf) 1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️‍♂️ man detective +#SUPPORT(prf) 1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻‍♂️ man detective: light skin tone +#SUPPORT(prf) 1F575 1F3FC 200D 2642 FE0F ; fully-qualified # 🕵🏼‍♂️ man detective: medium-light skin tone +#SUPPORT(prf) 1F575 1F3FD 200D 2642 FE0F ; fully-qualified # 🕵🏽‍♂️ man detective: medium skin tone +#SUPPORT(prf) 1F575 1F3FE 200D 2642 FE0F ; fully-qualified # 🕵🏾‍♂️ man detective: medium-dark skin tone +#SUPPORT(prf) 1F575 1F3FF 200D 2642 FE0F ; fully-qualified # 🕵🏿‍♂️ man detective: dark skin tone +#SUPPORT(prf) 1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️‍♀️ woman detective +#SUPPORT(prf) 1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻‍♀️ woman detective: light skin tone +#SUPPORT(prf) 1F575 1F3FC 200D 2640 FE0F ; fully-qualified # 🕵🏼‍♀️ woman detective: medium-light skin tone +#SUPPORT(prf) 1F575 1F3FD 200D 2640 FE0F ; fully-qualified # 🕵🏽‍♀️ woman detective: medium skin tone +#SUPPORT(prf) 1F575 1F3FE 200D 2640 FE0F ; fully-qualified # 🕵🏾‍♀️ woman detective: medium-dark skin tone +#SUPPORT(prf) 1F575 1F3FF 200D 2640 FE0F ; fully-qualified # 🕵🏿‍♀️ woman detective: dark skin tone +1F482 ; fully-qualified # 💂 guard +1F482 1F3FB ; fully-qualified # 💂🏻 guard: light skin tone +1F482 1F3FC ; fully-qualified # 💂🏼 guard: medium-light skin tone +1F482 1F3FD ; fully-qualified # 💂🏽 guard: medium skin tone +1F482 1F3FE ; fully-qualified # 💂🏾 guard: medium-dark skin tone +1F482 1F3FF ; fully-qualified # 💂🏿 guard: dark skin tone +#SUPPORT(prf) 1F482 200D 2642 FE0F ; fully-qualified # 💂‍♂️ man guard +#SUPPORT(prf) 1F482 1F3FB 200D 2642 FE0F ; fully-qualified # 💂🏻‍♂️ man guard: light skin tone +#SUPPORT(prf) 1F482 1F3FC 200D 2642 FE0F ; fully-qualified # 💂🏼‍♂️ man guard: medium-light skin tone +#SUPPORT(prf) 1F482 1F3FD 200D 2642 FE0F ; fully-qualified # 💂🏽‍♂️ man guard: medium skin tone +#SUPPORT(prf) 1F482 1F3FE 200D 2642 FE0F ; fully-qualified # 💂🏾‍♂️ man guard: medium-dark skin tone +#SUPPORT(prf) 1F482 1F3FF 200D 2642 FE0F ; fully-qualified # 💂🏿‍♂️ man guard: dark skin tone +#SUPPORT(prf) 1F482 200D 2640 FE0F ; fully-qualified # 💂‍♀️ woman guard +#SUPPORT(prf) 1F482 1F3FB 200D 2640 FE0F ; fully-qualified # 💂🏻‍♀️ woman guard: light skin tone +#SUPPORT(prf) 1F482 1F3FC 200D 2640 FE0F ; fully-qualified # 💂🏼‍♀️ woman guard: medium-light skin tone +#SUPPORT(prf) 1F482 1F3FD 200D 2640 FE0F ; fully-qualified # 💂🏽‍♀️ woman guard: medium skin tone +#SUPPORT(prf) 1F482 1F3FE 200D 2640 FE0F ; fully-qualified # 💂🏾‍♀️ woman guard: medium-dark skin tone +#SUPPORT(prf) 1F482 1F3FF 200D 2640 FE0F ; fully-qualified # 💂🏿‍♀️ woman guard: dark skin tone +1F477 ; fully-qualified # 👷 construction worker +1F477 1F3FB ; fully-qualified # 👷🏻 construction worker: light skin tone +1F477 1F3FC ; fully-qualified # 👷🏼 construction worker: medium-light skin tone +1F477 1F3FD ; fully-qualified # 👷🏽 construction worker: medium skin tone +1F477 1F3FE ; fully-qualified # 👷🏾 construction worker: medium-dark skin tone +1F477 1F3FF ; fully-qualified # 👷🏿 construction worker: dark skin tone +#SUPPORT(prf) 1F477 200D 2642 FE0F ; fully-qualified # 👷‍♂️ man construction worker +#SUPPORT(prf) 1F477 1F3FB 200D 2642 FE0F ; fully-qualified # 👷🏻‍♂️ man construction worker: light skin tone +#SUPPORT(prf) 1F477 1F3FC 200D 2642 FE0F ; fully-qualified # 👷🏼‍♂️ man construction worker: medium-light skin tone +#SUPPORT(prf) 1F477 1F3FD 200D 2642 FE0F ; fully-qualified # 👷🏽‍♂️ man construction worker: medium skin tone +#SUPPORT(prf) 1F477 1F3FE 200D 2642 FE0F ; fully-qualified # 👷🏾‍♂️ man construction worker: medium-dark skin tone +#SUPPORT(prf) 1F477 1F3FF 200D 2642 FE0F ; fully-qualified # 👷🏿‍♂️ man construction worker: dark skin tone +#SUPPORT(prf) 1F477 200D 2640 FE0F ; fully-qualified # 👷‍♀️ woman construction worker +#SUPPORT(prf) 1F477 1F3FB 200D 2640 FE0F ; fully-qualified # 👷🏻‍♀️ woman construction worker: light skin tone +#SUPPORT(prf) 1F477 1F3FC 200D 2640 FE0F ; fully-qualified # 👷🏼‍♀️ woman construction worker: medium-light skin tone +#SUPPORT(prf) 1F477 1F3FD 200D 2640 FE0F ; fully-qualified # 👷🏽‍♀️ woman construction worker: medium skin tone +#SUPPORT(prf) 1F477 1F3FE 200D 2640 FE0F ; fully-qualified # 👷🏾‍♀️ woman construction worker: medium-dark skin tone +#SUPPORT(prf) 1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿‍♀️ woman construction worker: dark skin tone +1F934 ; fully-qualified # 🤴 prince +1F934 1F3FB ; fully-qualified # 🤴🏻 prince: light skin tone +1F934 1F3FC ; fully-qualified # 🤴🏼 prince: medium-light skin tone +1F934 1F3FD ; fully-qualified # 🤴🏽 prince: medium skin tone +1F934 1F3FE ; fully-qualified # 🤴🏾 prince: medium-dark skin tone +1F934 1F3FF ; fully-qualified # 🤴🏿 prince: dark skin tone +1F478 ; fully-qualified # 👸 princess +1F478 1F3FB ; fully-qualified # 👸🏻 princess: light skin tone +1F478 1F3FC ; fully-qualified # 👸🏼 princess: medium-light skin tone +1F478 1F3FD ; fully-qualified # 👸🏽 princess: medium skin tone +1F478 1F3FE ; fully-qualified # 👸🏾 princess: medium-dark skin tone +1F478 1F3FF ; fully-qualified # 👸🏿 princess: dark skin tone +1F473 ; fully-qualified # 👳 person wearing turban +1F473 1F3FB ; fully-qualified # 👳🏻 person wearing turban: light skin tone +1F473 1F3FC ; fully-qualified # 👳🏼 person wearing turban: medium-light skin tone +1F473 1F3FD ; fully-qualified # 👳🏽 person wearing turban: medium skin tone +1F473 1F3FE ; fully-qualified # 👳🏾 person wearing turban: medium-dark skin tone +1F473 1F3FF ; fully-qualified # 👳🏿 person wearing turban: dark skin tone +#SUPPORT(prf) 1F473 200D 2642 FE0F ; fully-qualified # 👳‍♂️ man wearing turban +#SUPPORT(prf) 1F473 1F3FB 200D 2642 FE0F ; fully-qualified # 👳🏻‍♂️ man wearing turban: light skin tone +#SUPPORT(prf) 1F473 1F3FC 200D 2642 FE0F ; fully-qualified # 👳🏼‍♂️ man wearing turban: medium-light skin tone +#SUPPORT(prf) 1F473 1F3FD 200D 2642 FE0F ; fully-qualified # 👳🏽‍♂️ man wearing turban: medium skin tone +#SUPPORT(prf) 1F473 1F3FE 200D 2642 FE0F ; fully-qualified # 👳🏾‍♂️ man wearing turban: medium-dark skin tone +#SUPPORT(prf) 1F473 1F3FF 200D 2642 FE0F ; fully-qualified # 👳🏿‍♂️ man wearing turban: dark skin tone +#SUPPORT(prf) 1F473 200D 2640 FE0F ; fully-qualified # 👳‍♀️ woman wearing turban +#SUPPORT(prf) 1F473 1F3FB 200D 2640 FE0F ; fully-qualified # 👳🏻‍♀️ woman wearing turban: light skin tone +#SUPPORT(prf) 1F473 1F3FC 200D 2640 FE0F ; fully-qualified # 👳🏼‍♀️ woman wearing turban: medium-light skin tone +#SUPPORT(prf) 1F473 1F3FD 200D 2640 FE0F ; fully-qualified # 👳🏽‍♀️ woman wearing turban: medium skin tone +#SUPPORT(prf) 1F473 1F3FE 200D 2640 FE0F ; fully-qualified # 👳🏾‍♀️ woman wearing turban: medium-dark skin tone +#SUPPORT(prf) 1F473 1F3FF 200D 2640 FE0F ; fully-qualified # 👳🏿‍♀️ woman wearing turban: dark skin tone +1F472 ; fully-qualified # 👲 man with Chinese cap +1F472 1F3FB ; fully-qualified # 👲🏻 man with Chinese cap: light skin tone +1F472 1F3FC ; fully-qualified # 👲🏼 man with Chinese cap: medium-light skin tone +1F472 1F3FD ; fully-qualified # 👲🏽 man with Chinese cap: medium skin tone +1F472 1F3FE ; fully-qualified # 👲🏾 man with Chinese cap: medium-dark skin tone +1F472 1F3FF ; fully-qualified # 👲🏿 man with Chinese cap: dark skin tone +1F9D5 ; fully-qualified # 🧕 woman with headscarf +1F9D5 1F3FB ; fully-qualified # 🧕🏻 woman with headscarf: light skin tone +1F9D5 1F3FC ; fully-qualified # 🧕🏼 woman with headscarf: medium-light skin tone +1F9D5 1F3FD ; fully-qualified # 🧕🏽 woman with headscarf: medium skin tone +1F9D5 1F3FE ; fully-qualified # 🧕🏾 woman with headscarf: medium-dark skin tone +1F9D5 1F3FF ; fully-qualified # 🧕🏿 woman with headscarf: dark skin tone +1F935 ; fully-qualified # 🤵 man in tuxedo +1F935 1F3FB ; fully-qualified # 🤵🏻 man in tuxedo: light skin tone +1F935 1F3FC ; fully-qualified # 🤵🏼 man in tuxedo: medium-light skin tone +1F935 1F3FD ; fully-qualified # 🤵🏽 man in tuxedo: medium skin tone +1F935 1F3FE ; fully-qualified # 🤵🏾 man in tuxedo: medium-dark skin tone +1F935 1F3FF ; fully-qualified # 🤵🏿 man in tuxedo: dark skin tone +1F470 ; fully-qualified # 👰 bride with veil +1F470 1F3FB ; fully-qualified # 👰🏻 bride with veil: light skin tone +1F470 1F3FC ; fully-qualified # 👰🏼 bride with veil: medium-light skin tone +1F470 1F3FD ; fully-qualified # 👰🏽 bride with veil: medium skin tone +1F470 1F3FE ; fully-qualified # 👰🏾 bride with veil: medium-dark skin tone +1F470 1F3FF ; fully-qualified # 👰🏿 bride with veil: dark skin tone +1F930 ; fully-qualified # 🤰 pregnant woman +1F930 1F3FB ; fully-qualified # 🤰🏻 pregnant woman: light skin tone +1F930 1F3FC ; fully-qualified # 🤰🏼 pregnant woman: medium-light skin tone +1F930 1F3FD ; fully-qualified # 🤰🏽 pregnant woman: medium skin tone +1F930 1F3FE ; fully-qualified # 🤰🏾 pregnant woman: medium-dark skin tone +1F930 1F3FF ; fully-qualified # 🤰🏿 pregnant woman: dark skin tone +1F931 ; fully-qualified # 🤱 breast-feeding +1F931 1F3FB ; fully-qualified # 🤱🏻 breast-feeding: light skin tone +1F931 1F3FC ; fully-qualified # 🤱🏼 breast-feeding: medium-light skin tone +1F931 1F3FD ; fully-qualified # 🤱🏽 breast-feeding: medium skin tone +1F931 1F3FE ; fully-qualified # 🤱🏾 breast-feeding: medium-dark skin tone +1F931 1F3FF ; fully-qualified # 🤱🏿 breast-feeding: dark skin tone + +# subgroup: person-fantasy +1F47C ; fully-qualified # 👼 baby angel +1F47C 1F3FB ; fully-qualified # 👼🏻 baby angel: light skin tone +1F47C 1F3FC ; fully-qualified # 👼🏼 baby angel: medium-light skin tone +1F47C 1F3FD ; fully-qualified # 👼🏽 baby angel: medium skin tone +1F47C 1F3FE ; fully-qualified # 👼🏾 baby angel: medium-dark skin tone +1F47C 1F3FF ; fully-qualified # 👼🏿 baby angel: dark skin tone +1F385 ; fully-qualified # 🎅 Santa Claus +1F385 1F3FB ; fully-qualified # 🎅🏻 Santa Claus: light skin tone +1F385 1F3FC ; fully-qualified # 🎅🏼 Santa Claus: medium-light skin tone +1F385 1F3FD ; fully-qualified # 🎅🏽 Santa Claus: medium skin tone +1F385 1F3FE ; fully-qualified # 🎅🏾 Santa Claus: medium-dark skin tone +1F385 1F3FF ; fully-qualified # 🎅🏿 Santa Claus: dark skin tone +1F936 ; fully-qualified # 🤶 Mrs. Claus +1F936 1F3FB ; fully-qualified # 🤶🏻 Mrs. Claus: light skin tone +1F936 1F3FC ; fully-qualified # 🤶🏼 Mrs. Claus: medium-light skin tone +1F936 1F3FD ; fully-qualified # 🤶🏽 Mrs. Claus: medium skin tone +1F936 1F3FE ; fully-qualified # 🤶🏾 Mrs. Claus: medium-dark skin tone +1F936 1F3FF ; fully-qualified # 🤶🏿 Mrs. Claus: dark skin tone +#SUPPORT(prf) 1F9B8 ; fully-qualified # 🦸 superhero +#SUPPORT(prf) 1F9B8 1F3FB ; fully-qualified # 🦸🏻 superhero: light skin tone +#SUPPORT(prf) 1F9B8 1F3FC ; fully-qualified # 🦸🏼 superhero: medium-light skin tone +#SUPPORT(prf) 1F9B8 1F3FD ; fully-qualified # 🦸🏽 superhero: medium skin tone +#SUPPORT(prf) 1F9B8 1F3FE ; fully-qualified # 🦸🏾 superhero: medium-dark skin tone +#SUPPORT(prf) 1F9B8 1F3FF ; fully-qualified # 🦸🏿 superhero: dark skin tone +#SUPPORT(prf) 1F9B8 200D 2642 FE0F ; fully-qualified # 🦸‍♂️ man superhero +#SUPPORT(prf) 1F9B8 1F3FB 200D 2642 FE0F ; fully-qualified # 🦸🏻‍♂️ man superhero: light skin tone +#SUPPORT(prf) 1F9B8 1F3FC 200D 2642 FE0F ; fully-qualified # 🦸🏼‍♂️ man superhero: medium-light skin tone +#SUPPORT(prf) 1F9B8 1F3FD 200D 2642 FE0F ; fully-qualified # 🦸🏽‍♂️ man superhero: medium skin tone +#SUPPORT(prf) 1F9B8 1F3FE 200D 2642 FE0F ; fully-qualified # 🦸🏾‍♂️ man superhero: medium-dark skin tone +#SUPPORT(prf) 1F9B8 1F3FF 200D 2642 FE0F ; fully-qualified # 🦸🏿‍♂️ man superhero: dark skin tone +#SUPPORT(prf) 1F9B8 200D 2640 FE0F ; fully-qualified # 🦸‍♀️ woman superhero +#SUPPORT(prf) 1F9B8 1F3FB 200D 2640 FE0F ; fully-qualified # 🦸🏻‍♀️ woman superhero: light skin tone +#SUPPORT(prf) 1F9B8 1F3FC 200D 2640 FE0F ; fully-qualified # 🦸🏼‍♀️ woman superhero: medium-light skin tone +#SUPPORT(prf) 1F9B8 1F3FD 200D 2640 FE0F ; fully-qualified # 🦸🏽‍♀️ woman superhero: medium skin tone +#SUPPORT(prf) 1F9B8 1F3FE 200D 2640 FE0F ; fully-qualified # 🦸🏾‍♀️ woman superhero: medium-dark skin tone +#SUPPORT(prf) 1F9B8 1F3FF 200D 2640 FE0F ; fully-qualified # 🦸🏿‍♀️ woman superhero: dark skin tone +#SUPPORT(prf) 1F9B9 ; fully-qualified # 🦹 supervillain +#SUPPORT(prf) 1F9B9 1F3FB ; fully-qualified # 🦹🏻 supervillain: light skin tone +#SUPPORT(prf) 1F9B9 1F3FC ; fully-qualified # 🦹🏼 supervillain: medium-light skin tone +#SUPPORT(prf) 1F9B9 1F3FD ; fully-qualified # 🦹🏽 supervillain: medium skin tone +#SUPPORT(prf) 1F9B9 1F3FE ; fully-qualified # 🦹🏾 supervillain: medium-dark skin tone +#SUPPORT(prf) 1F9B9 1F3FF ; fully-qualified # 🦹🏿 supervillain: dark skin tone +#SUPPORT(prf) 1F9B9 200D 2642 FE0F ; fully-qualified # 🦹‍♂️ man supervillain +#SUPPORT(prf) 1F9B9 1F3FB 200D 2642 FE0F ; fully-qualified # 🦹🏻‍♂️ man supervillain: light skin tone +#SUPPORT(prf) 1F9B9 1F3FC 200D 2642 FE0F ; fully-qualified # 🦹🏼‍♂️ man supervillain: medium-light skin tone +#SUPPORT(prf) 1F9B9 1F3FD 200D 2642 FE0F ; fully-qualified # 🦹🏽‍♂️ man supervillain: medium skin tone +#SUPPORT(prf) 1F9B9 1F3FE 200D 2642 FE0F ; fully-qualified # 🦹🏾‍♂️ man supervillain: medium-dark skin tone +#SUPPORT(prf) 1F9B9 1F3FF 200D 2642 FE0F ; fully-qualified # 🦹🏿‍♂️ man supervillain: dark skin tone +#SUPPORT(prf) 1F9B9 200D 2640 FE0F ; fully-qualified # 🦹‍♀️ woman supervillain +#SUPPORT(prf) 1F9B9 1F3FB 200D 2640 FE0F ; fully-qualified # 🦹🏻‍♀️ woman supervillain: light skin tone +#SUPPORT(prf) 1F9B9 1F3FC 200D 2640 FE0F ; fully-qualified # 🦹🏼‍♀️ woman supervillain: medium-light skin tone +#SUPPORT(prf) 1F9B9 1F3FD 200D 2640 FE0F ; fully-qualified # 🦹🏽‍♀️ woman supervillain: medium skin tone +#SUPPORT(prf) 1F9B9 1F3FE 200D 2640 FE0F ; fully-qualified # 🦹🏾‍♀️ woman supervillain: medium-dark skin tone +#SUPPORT(prf) 1F9B9 1F3FF 200D 2640 FE0F ; fully-qualified # 🦹🏿‍♀️ woman supervillain: dark skin tone +1F9D9 ; fully-qualified # 🧙 mage +1F9D9 1F3FB ; fully-qualified # 🧙🏻 mage: light skin tone +1F9D9 1F3FC ; fully-qualified # 🧙🏼 mage: medium-light skin tone +1F9D9 1F3FD ; fully-qualified # 🧙🏽 mage: medium skin tone +1F9D9 1F3FE ; fully-qualified # 🧙🏾 mage: medium-dark skin tone +1F9D9 1F3FF ; fully-qualified # 🧙🏿 mage: dark skin tone +#SUPPORT(prf) 1F9D9 200D 2642 FE0F ; fully-qualified # 🧙‍♂️ man mage +#SUPPORT(prf) 1F9D9 1F3FB 200D 2642 FE0F ; fully-qualified # 🧙🏻‍♂️ man mage: light skin tone +#SUPPORT(prf) 1F9D9 1F3FC 200D 2642 FE0F ; fully-qualified # 🧙🏼‍♂️ man mage: medium-light skin tone +#SUPPORT(prf) 1F9D9 1F3FD 200D 2642 FE0F ; fully-qualified # 🧙🏽‍♂️ man mage: medium skin tone +#SUPPORT(prf) 1F9D9 1F3FE 200D 2642 FE0F ; fully-qualified # 🧙🏾‍♂️ man mage: medium-dark skin tone +#SUPPORT(prf) 1F9D9 1F3FF 200D 2642 FE0F ; fully-qualified # 🧙🏿‍♂️ man mage: dark skin tone +#SUPPORT(prf) 1F9D9 200D 2640 FE0F ; fully-qualified # 🧙‍♀️ woman mage +#SUPPORT(prf) 1F9D9 1F3FB 200D 2640 FE0F ; fully-qualified # 🧙🏻‍♀️ woman mage: light skin tone +#SUPPORT(prf) 1F9D9 1F3FC 200D 2640 FE0F ; fully-qualified # 🧙🏼‍♀️ woman mage: medium-light skin tone +#SUPPORT(prf) 1F9D9 1F3FD 200D 2640 FE0F ; fully-qualified # 🧙🏽‍♀️ woman mage: medium skin tone +#SUPPORT(prf) 1F9D9 1F3FE 200D 2640 FE0F ; fully-qualified # 🧙🏾‍♀️ woman mage: medium-dark skin tone +#SUPPORT(prf) 1F9D9 1F3FF 200D 2640 FE0F ; fully-qualified # 🧙🏿‍♀️ woman mage: dark skin tone +1F9DA ; fully-qualified # 🧚 fairy +1F9DA 1F3FB ; fully-qualified # 🧚🏻 fairy: light skin tone +1F9DA 1F3FC ; fully-qualified # 🧚🏼 fairy: medium-light skin tone +1F9DA 1F3FD ; fully-qualified # 🧚🏽 fairy: medium skin tone +1F9DA 1F3FE ; fully-qualified # 🧚🏾 fairy: medium-dark skin tone +1F9DA 1F3FF ; fully-qualified # 🧚🏿 fairy: dark skin tone +#SUPPORT(prf) 1F9DA 200D 2642 FE0F ; fully-qualified # 🧚‍♂️ man fairy +#SUPPORT(prf) 1F9DA 1F3FB 200D 2642 FE0F ; fully-qualified # 🧚🏻‍♂️ man fairy: light skin tone +#SUPPORT(prf) 1F9DA 1F3FC 200D 2642 FE0F ; fully-qualified # 🧚🏼‍♂️ man fairy: medium-light skin tone +#SUPPORT(prf) 1F9DA 1F3FD 200D 2642 FE0F ; fully-qualified # 🧚🏽‍♂️ man fairy: medium skin tone +#SUPPORT(prf) 1F9DA 1F3FE 200D 2642 FE0F ; fully-qualified # 🧚🏾‍♂️ man fairy: medium-dark skin tone +#SUPPORT(prf) 1F9DA 1F3FF 200D 2642 FE0F ; fully-qualified # 🧚🏿‍♂️ man fairy: dark skin tone +#SUPPORT(prf) 1F9DA 200D 2640 FE0F ; fully-qualified # 🧚‍♀️ woman fairy +#SUPPORT(prf) 1F9DA 1F3FB 200D 2640 FE0F ; fully-qualified # 🧚🏻‍♀️ woman fairy: light skin tone +#SUPPORT(prf) 1F9DA 1F3FC 200D 2640 FE0F ; fully-qualified # 🧚🏼‍♀️ woman fairy: medium-light skin tone +#SUPPORT(prf) 1F9DA 1F3FD 200D 2640 FE0F ; fully-qualified # 🧚🏽‍♀️ woman fairy: medium skin tone +#SUPPORT(prf) 1F9DA 1F3FE 200D 2640 FE0F ; fully-qualified # 🧚🏾‍♀️ woman fairy: medium-dark skin tone +#SUPPORT(prf) 1F9DA 1F3FF 200D 2640 FE0F ; fully-qualified # 🧚🏿‍♀️ woman fairy: dark skin tone +1F9DB ; fully-qualified # 🧛 vampire +1F9DB 1F3FB ; fully-qualified # 🧛🏻 vampire: light skin tone +1F9DB 1F3FC ; fully-qualified # 🧛🏼 vampire: medium-light skin tone +1F9DB 1F3FD ; fully-qualified # 🧛🏽 vampire: medium skin tone +1F9DB 1F3FE ; fully-qualified # 🧛🏾 vampire: medium-dark skin tone +1F9DB 1F3FF ; fully-qualified # 🧛🏿 vampire: dark skin tone +#SUPPORT(prf) 1F9DB 200D 2642 FE0F ; fully-qualified # 🧛‍♂️ man vampire +#SUPPORT(prf) 1F9DB 1F3FB 200D 2642 FE0F ; fully-qualified # 🧛🏻‍♂️ man vampire: light skin tone +#SUPPORT(prf) 1F9DB 1F3FC 200D 2642 FE0F ; fully-qualified # 🧛🏼‍♂️ man vampire: medium-light skin tone +#SUPPORT(prf) 1F9DB 1F3FD 200D 2642 FE0F ; fully-qualified # 🧛🏽‍♂️ man vampire: medium skin tone +#SUPPORT(prf) 1F9DB 1F3FE 200D 2642 FE0F ; fully-qualified # 🧛🏾‍♂️ man vampire: medium-dark skin tone +#SUPPORT(prf) 1F9DB 1F3FF 200D 2642 FE0F ; fully-qualified # 🧛🏿‍♂️ man vampire: dark skin tone +#SUPPORT(prf) 1F9DB 200D 2640 FE0F ; fully-qualified # 🧛‍♀️ woman vampire +#SUPPORT(prf) 1F9DB 1F3FB 200D 2640 FE0F ; fully-qualified # 🧛🏻‍♀️ woman vampire: light skin tone +#SUPPORT(prf) 1F9DB 1F3FC 200D 2640 FE0F ; fully-qualified # 🧛🏼‍♀️ woman vampire: medium-light skin tone +#SUPPORT(prf) 1F9DB 1F3FD 200D 2640 FE0F ; fully-qualified # 🧛🏽‍♀️ woman vampire: medium skin tone +#SUPPORT(prf) 1F9DB 1F3FE 200D 2640 FE0F ; fully-qualified # 🧛🏾‍♀️ woman vampire: medium-dark skin tone +#SUPPORT(prf) 1F9DB 1F3FF 200D 2640 FE0F ; fully-qualified # 🧛🏿‍♀️ woman vampire: dark skin tone +1F9DC ; fully-qualified # 🧜 merperson +1F9DC 1F3FB ; fully-qualified # 🧜🏻 merperson: light skin tone +1F9DC 1F3FC ; fully-qualified # 🧜🏼 merperson: medium-light skin tone +1F9DC 1F3FD ; fully-qualified # 🧜🏽 merperson: medium skin tone +1F9DC 1F3FE ; fully-qualified # 🧜🏾 merperson: medium-dark skin tone +1F9DC 1F3FF ; fully-qualified # 🧜🏿 merperson: dark skin tone +#SUPPORT(prf) 1F9DC 200D 2642 FE0F ; fully-qualified # 🧜‍♂️ merman +#SUPPORT(prf) 1F9DC 1F3FB 200D 2642 FE0F ; fully-qualified # 🧜🏻‍♂️ merman: light skin tone +#SUPPORT(prf) 1F9DC 1F3FC 200D 2642 FE0F ; fully-qualified # 🧜🏼‍♂️ merman: medium-light skin tone +#SUPPORT(prf) 1F9DC 1F3FD 200D 2642 FE0F ; fully-qualified # 🧜🏽‍♂️ merman: medium skin tone +#SUPPORT(prf) 1F9DC 1F3FE 200D 2642 FE0F ; fully-qualified # 🧜🏾‍♂️ merman: medium-dark skin tone +#SUPPORT(prf) 1F9DC 1F3FF 200D 2642 FE0F ; fully-qualified # 🧜🏿‍♂️ merman: dark skin tone +#SUPPORT(prf) 1F9DC 200D 2640 FE0F ; fully-qualified # 🧜‍♀️ mermaid +#SUPPORT(prf) 1F9DC 1F3FB 200D 2640 FE0F ; fully-qualified # 🧜🏻‍♀️ mermaid: light skin tone +#SUPPORT(prf) 1F9DC 1F3FC 200D 2640 FE0F ; fully-qualified # 🧜🏼‍♀️ mermaid: medium-light skin tone +#SUPPORT(prf) 1F9DC 1F3FD 200D 2640 FE0F ; fully-qualified # 🧜🏽‍♀️ mermaid: medium skin tone +#SUPPORT(prf) 1F9DC 1F3FE 200D 2640 FE0F ; fully-qualified # 🧜🏾‍♀️ mermaid: medium-dark skin tone +#SUPPORT(prf) 1F9DC 1F3FF 200D 2640 FE0F ; fully-qualified # 🧜🏿‍♀️ mermaid: dark skin tone +1F9DD ; fully-qualified # 🧝 elf +1F9DD 1F3FB ; fully-qualified # 🧝🏻 elf: light skin tone +1F9DD 1F3FC ; fully-qualified # 🧝🏼 elf: medium-light skin tone +1F9DD 1F3FD ; fully-qualified # 🧝🏽 elf: medium skin tone +1F9DD 1F3FE ; fully-qualified # 🧝🏾 elf: medium-dark skin tone +1F9DD 1F3FF ; fully-qualified # 🧝🏿 elf: dark skin tone +#SUPPORT(prf) 1F9DD 200D 2642 FE0F ; fully-qualified # 🧝‍♂️ man elf +#SUPPORT(prf) 1F9DD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧝🏻‍♂️ man elf: light skin tone +#SUPPORT(prf) 1F9DD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧝🏼‍♂️ man elf: medium-light skin tone +#SUPPORT(prf) 1F9DD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧝🏽‍♂️ man elf: medium skin tone +#SUPPORT(prf) 1F9DD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧝🏾‍♂️ man elf: medium-dark skin tone +#SUPPORT(prf) 1F9DD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧝🏿‍♂️ man elf: dark skin tone +#SUPPORT(prf) 1F9DD 200D 2640 FE0F ; fully-qualified # 🧝‍♀️ woman elf +#SUPPORT(prf) 1F9DD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧝🏻‍♀️ woman elf: light skin tone +#SUPPORT(prf) 1F9DD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧝🏼‍♀️ woman elf: medium-light skin tone +#SUPPORT(prf) 1F9DD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧝🏽‍♀️ woman elf: medium skin tone +#SUPPORT(prf) 1F9DD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧝🏾‍♀️ woman elf: medium-dark skin tone +#SUPPORT(prf) 1F9DD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧝🏿‍♀️ woman elf: dark skin tone +1F9DE ; fully-qualified # 🧞 genie +#SUPPORT(prf) 1F9DE 200D 2642 FE0F ; fully-qualified # 🧞‍♂️ man genie +#SUPPORT(prf) 1F9DE 200D 2640 FE0F ; fully-qualified # 🧞‍♀️ woman genie +1F9DF ; fully-qualified # 🧟 zombie +#SUPPORT(prf) 1F9DF 200D 2642 FE0F ; fully-qualified # 🧟‍♂️ man zombie +#SUPPORT(prf) 1F9DF 200D 2640 FE0F ; fully-qualified # 🧟‍♀️ woman zombie + +# subgroup: person-activity +1F486 ; fully-qualified # 💆 person getting massage +1F486 1F3FB ; fully-qualified # 💆🏻 person getting massage: light skin tone +1F486 1F3FC ; fully-qualified # 💆🏼 person getting massage: medium-light skin tone +1F486 1F3FD ; fully-qualified # 💆🏽 person getting massage: medium skin tone +1F486 1F3FE ; fully-qualified # 💆🏾 person getting massage: medium-dark skin tone +1F486 1F3FF ; fully-qualified # 💆🏿 person getting massage: dark skin tone +#SUPPORT(prf) 1F486 200D 2642 FE0F ; fully-qualified # 💆‍♂️ man getting massage +#SUPPORT(prf) 1F486 1F3FB 200D 2642 FE0F ; fully-qualified # 💆🏻‍♂️ man getting massage: light skin tone +#SUPPORT(prf) 1F486 1F3FC 200D 2642 FE0F ; fully-qualified # 💆🏼‍♂️ man getting massage: medium-light skin tone +#SUPPORT(prf) 1F486 1F3FD 200D 2642 FE0F ; fully-qualified # 💆🏽‍♂️ man getting massage: medium skin tone +#SUPPORT(prf) 1F486 1F3FE 200D 2642 FE0F ; fully-qualified # 💆🏾‍♂️ man getting massage: medium-dark skin tone +#SUPPORT(prf) 1F486 1F3FF 200D 2642 FE0F ; fully-qualified # 💆🏿‍♂️ man getting massage: dark skin tone +#SUPPORT(prf) 1F486 200D 2640 FE0F ; fully-qualified # 💆‍♀️ woman getting massage +#SUPPORT(prf) 1F486 1F3FB 200D 2640 FE0F ; fully-qualified # 💆🏻‍♀️ woman getting massage: light skin tone +#SUPPORT(prf) 1F486 1F3FC 200D 2640 FE0F ; fully-qualified # 💆🏼‍♀️ woman getting massage: medium-light skin tone +#SUPPORT(prf) 1F486 1F3FD 200D 2640 FE0F ; fully-qualified # 💆🏽‍♀️ woman getting massage: medium skin tone +#SUPPORT(prf) 1F486 1F3FE 200D 2640 FE0F ; fully-qualified # 💆🏾‍♀️ woman getting massage: medium-dark skin tone +#SUPPORT(prf) 1F486 1F3FF 200D 2640 FE0F ; fully-qualified # 💆🏿‍♀️ woman getting massage: dark skin tone +1F487 ; fully-qualified # 💇 person getting haircut +1F487 1F3FB ; fully-qualified # 💇🏻 person getting haircut: light skin tone +1F487 1F3FC ; fully-qualified # 💇🏼 person getting haircut: medium-light skin tone +1F487 1F3FD ; fully-qualified # 💇🏽 person getting haircut: medium skin tone +1F487 1F3FE ; fully-qualified # 💇🏾 person getting haircut: medium-dark skin tone +1F487 1F3FF ; fully-qualified # 💇🏿 person getting haircut: dark skin tone +#SUPPORT(prf) 1F487 200D 2642 FE0F ; fully-qualified # 💇‍♂️ man getting haircut +#SUPPORT(prf) 1F487 1F3FB 200D 2642 FE0F ; fully-qualified # 💇🏻‍♂️ man getting haircut: light skin tone +#SUPPORT(prf) 1F487 1F3FC 200D 2642 FE0F ; fully-qualified # 💇🏼‍♂️ man getting haircut: medium-light skin tone +#SUPPORT(prf) 1F487 1F3FD 200D 2642 FE0F ; fully-qualified # 💇🏽‍♂️ man getting haircut: medium skin tone +#SUPPORT(prf) 1F487 1F3FE 200D 2642 FE0F ; fully-qualified # 💇🏾‍♂️ man getting haircut: medium-dark skin tone +#SUPPORT(prf) 1F487 1F3FF 200D 2642 FE0F ; fully-qualified # 💇🏿‍♂️ man getting haircut: dark skin tone +#SUPPORT(prf) 1F487 200D 2640 FE0F ; fully-qualified # 💇‍♀️ woman getting haircut +#SUPPORT(prf) 1F487 1F3FB 200D 2640 FE0F ; fully-qualified # 💇🏻‍♀️ woman getting haircut: light skin tone +#SUPPORT(prf) 1F487 1F3FC 200D 2640 FE0F ; fully-qualified # 💇🏼‍♀️ woman getting haircut: medium-light skin tone +#SUPPORT(prf) 1F487 1F3FD 200D 2640 FE0F ; fully-qualified # 💇🏽‍♀️ woman getting haircut: medium skin tone +#SUPPORT(prf) 1F487 1F3FE 200D 2640 FE0F ; fully-qualified # 💇🏾‍♀️ woman getting haircut: medium-dark skin tone +#SUPPORT(prf) 1F487 1F3FF 200D 2640 FE0F ; fully-qualified # 💇🏿‍♀️ woman getting haircut: dark skin tone +1F6B6 ; fully-qualified # 🚶 person walking +1F6B6 1F3FB ; fully-qualified # 🚶🏻 person walking: light skin tone +1F6B6 1F3FC ; fully-qualified # 🚶🏼 person walking: medium-light skin tone +1F6B6 1F3FD ; fully-qualified # 🚶🏽 person walking: medium skin tone +1F6B6 1F3FE ; fully-qualified # 🚶🏾 person walking: medium-dark skin tone +1F6B6 1F3FF ; fully-qualified # 🚶🏿 person walking: dark skin tone +#SUPPORT(prf) 1F6B6 200D 2642 FE0F ; fully-qualified # 🚶‍♂️ man walking +#SUPPORT(prf) 1F6B6 1F3FB 200D 2642 FE0F ; fully-qualified # 🚶🏻‍♂️ man walking: light skin tone +#SUPPORT(prf) 1F6B6 1F3FC 200D 2642 FE0F ; fully-qualified # 🚶🏼‍♂️ man walking: medium-light skin tone +#SUPPORT(prf) 1F6B6 1F3FD 200D 2642 FE0F ; fully-qualified # 🚶🏽‍♂️ man walking: medium skin tone +#SUPPORT(prf) 1F6B6 1F3FE 200D 2642 FE0F ; fully-qualified # 🚶🏾‍♂️ man walking: medium-dark skin tone +#SUPPORT(prf) 1F6B6 1F3FF 200D 2642 FE0F ; fully-qualified # 🚶🏿‍♂️ man walking: dark skin tone +#SUPPORT(prf) 1F6B6 200D 2640 FE0F ; fully-qualified # 🚶‍♀️ woman walking +#SUPPORT(prf) 1F6B6 1F3FB 200D 2640 FE0F ; fully-qualified # 🚶🏻‍♀️ woman walking: light skin tone +#SUPPORT(prf) 1F6B6 1F3FC 200D 2640 FE0F ; fully-qualified # 🚶🏼‍♀️ woman walking: medium-light skin tone +#SUPPORT(prf) 1F6B6 1F3FD 200D 2640 FE0F ; fully-qualified # 🚶🏽‍♀️ woman walking: medium skin tone +#SUPPORT(prf) 1F6B6 1F3FE 200D 2640 FE0F ; fully-qualified # 🚶🏾‍♀️ woman walking: medium-dark skin tone +#SUPPORT(prf) 1F6B6 1F3FF 200D 2640 FE0F ; fully-qualified # 🚶🏿‍♀️ woman walking: dark skin tone +#SUPPORT(prf) 1F9CD ; fully-qualified # 🧍 person standing +#SUPPORT(prf) 1F9CD 1F3FB ; fully-qualified # 🧍🏻 person standing: light skin tone +#SUPPORT(prf) 1F9CD 1F3FC ; fully-qualified # 🧍🏼 person standing: medium-light skin tone +#SUPPORT(prf) 1F9CD 1F3FD ; fully-qualified # 🧍🏽 person standing: medium skin tone +#SUPPORT(prf) 1F9CD 1F3FE ; fully-qualified # 🧍🏾 person standing: medium-dark skin tone +#SUPPORT(prf) 1F9CD 1F3FF ; fully-qualified # 🧍🏿 person standing: dark skin tone +#SUPPORT(prf) 1F9CD 200D 2642 FE0F ; fully-qualified # 🧍‍♂️ man standing +#SUPPORT(prf) 1F9CD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧍🏻‍♂️ man standing: light skin tone +#SUPPORT(prf) 1F9CD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧍🏼‍♂️ man standing: medium-light skin tone +#SUPPORT(prf) 1F9CD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧍🏽‍♂️ man standing: medium skin tone +#SUPPORT(prf) 1F9CD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧍🏾‍♂️ man standing: medium-dark skin tone +#SUPPORT(prf) 1F9CD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧍🏿‍♂️ man standing: dark skin tone +#SUPPORT(prf) 1F9CD 200D 2640 FE0F ; fully-qualified # 🧍‍♀️ woman standing +#SUPPORT(prf) 1F9CD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧍🏻‍♀️ woman standing: light skin tone +#SUPPORT(prf) 1F9CD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧍🏼‍♀️ woman standing: medium-light skin tone +#SUPPORT(prf) 1F9CD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧍🏽‍♀️ woman standing: medium skin tone +#SUPPORT(prf) 1F9CD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧍🏾‍♀️ woman standing: medium-dark skin tone +#SUPPORT(prf) 1F9CD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧍🏿‍♀️ woman standing: dark skin tone +#SUPPORT(prf) 1F9CE ; fully-qualified # 🧎 person kneeling +#SUPPORT(prf) 1F9CE 1F3FB ; fully-qualified # 🧎🏻 person kneeling: light skin tone +#SUPPORT(prf) 1F9CE 1F3FC ; fully-qualified # 🧎🏼 person kneeling: medium-light skin tone +#SUPPORT(prf) 1F9CE 1F3FD ; fully-qualified # 🧎🏽 person kneeling: medium skin tone +#SUPPORT(prf) 1F9CE 1F3FE ; fully-qualified # 🧎🏾 person kneeling: medium-dark skin tone +#SUPPORT(prf) 1F9CE 1F3FF ; fully-qualified # 🧎🏿 person kneeling: dark skin tone +#SUPPORT(prf) 1F9CE 200D 2642 FE0F ; fully-qualified # 🧎‍♂️ man kneeling +#SUPPORT(prf) 1F9CE 1F3FB 200D 2642 FE0F ; fully-qualified # 🧎🏻‍♂️ man kneeling: light skin tone +#SUPPORT(prf) 1F9CE 1F3FC 200D 2642 FE0F ; fully-qualified # 🧎🏼‍♂️ man kneeling: medium-light skin tone +#SUPPORT(prf) 1F9CE 1F3FD 200D 2642 FE0F ; fully-qualified # 🧎🏽‍♂️ man kneeling: medium skin tone +#SUPPORT(prf) 1F9CE 1F3FE 200D 2642 FE0F ; fully-qualified # 🧎🏾‍♂️ man kneeling: medium-dark skin tone +#SUPPORT(prf) 1F9CE 1F3FF 200D 2642 FE0F ; fully-qualified # 🧎🏿‍♂️ man kneeling: dark skin tone +#SUPPORT(prf) 1F9CE 200D 2640 FE0F ; fully-qualified # 🧎‍♀️ woman kneeling +#SUPPORT(prf) 1F9CE 1F3FB 200D 2640 FE0F ; fully-qualified # 🧎🏻‍♀️ woman kneeling: light skin tone +#SUPPORT(prf) 1F9CE 1F3FC 200D 2640 FE0F ; fully-qualified # 🧎🏼‍♀️ woman kneeling: medium-light skin tone +#SUPPORT(prf) 1F9CE 1F3FD 200D 2640 FE0F ; fully-qualified # 🧎🏽‍♀️ woman kneeling: medium skin tone +#SUPPORT(prf) 1F9CE 1F3FE 200D 2640 FE0F ; fully-qualified # 🧎🏾‍♀️ woman kneeling: medium-dark skin tone +#SUPPORT(prf) 1F9CE 1F3FF 200D 2640 FE0F ; fully-qualified # 🧎🏿‍♀️ woman kneeling: dark skin tone +#SUPPORT(prf) 1F468 200D 1F9AF ; fully-qualified # 👨‍🦯 man with probing cane +#SUPPORT(prf) 1F468 1F3FB 200D 1F9AF ; fully-qualified # 👨🏻‍🦯 man with probing cane: light skin tone +#SUPPORT(prf) 1F468 1F3FC 200D 1F9AF ; fully-qualified # 👨🏼‍🦯 man with probing cane: medium-light skin tone +#SUPPORT(prf) 1F468 1F3FD 200D 1F9AF ; fully-qualified # 👨🏽‍🦯 man with probing cane: medium skin tone +#SUPPORT(prf) 1F468 1F3FE 200D 1F9AF ; fully-qualified # 👨🏾‍🦯 man with probing cane: medium-dark skin tone +#SUPPORT(prf) 1F468 1F3FF 200D 1F9AF ; fully-qualified # 👨🏿‍🦯 man with probing cane: dark skin tone +#SUPPORT(prf) 1F469 200D 1F9AF ; fully-qualified # 👩‍🦯 woman with probing cane +#SUPPORT(prf) 1F469 1F3FB 200D 1F9AF ; fully-qualified # 👩🏻‍🦯 woman with probing cane: light skin tone +#SUPPORT(prf) 1F469 1F3FC 200D 1F9AF ; fully-qualified # 👩🏼‍🦯 woman with probing cane: medium-light skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F9AF ; fully-qualified # 👩🏽‍🦯 woman with probing cane: medium skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F9AF ; fully-qualified # 👩🏾‍🦯 woman with probing cane: medium-dark skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F9AF ; fully-qualified # 👩🏿‍🦯 woman with probing cane: dark skin tone +#SUPPORT(prf) 1F468 200D 1F9BC ; fully-qualified # 👨‍🦼 man in motorized wheelchair +#SUPPORT(prf) 1F468 1F3FB 200D 1F9BC ; fully-qualified # 👨🏻‍🦼 man in motorized wheelchair: light skin tone +#SUPPORT(prf) 1F468 1F3FC 200D 1F9BC ; fully-qualified # 👨🏼‍🦼 man in motorized wheelchair: medium-light skin tone +#SUPPORT(prf) 1F468 1F3FD 200D 1F9BC ; fully-qualified # 👨🏽‍🦼 man in motorized wheelchair: medium skin tone +#SUPPORT(prf) 1F468 1F3FE 200D 1F9BC ; fully-qualified # 👨🏾‍🦼 man in motorized wheelchair: medium-dark skin tone +#SUPPORT(prf) 1F468 1F3FF 200D 1F9BC ; fully-qualified # 👨🏿‍🦼 man in motorized wheelchair: dark skin tone +#SUPPORT(prf) 1F469 200D 1F9BC ; fully-qualified # 👩‍🦼 woman in motorized wheelchair +#SUPPORT(prf) 1F469 1F3FB 200D 1F9BC ; fully-qualified # 👩🏻‍🦼 woman in motorized wheelchair: light skin tone +#SUPPORT(prf) 1F469 1F3FC 200D 1F9BC ; fully-qualified # 👩🏼‍🦼 woman in motorized wheelchair: medium-light skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F9BC ; fully-qualified # 👩🏽‍🦼 woman in motorized wheelchair: medium skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F9BC ; fully-qualified # 👩🏾‍🦼 woman in motorized wheelchair: medium-dark skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F9BC ; fully-qualified # 👩🏿‍🦼 woman in motorized wheelchair: dark skin tone +#SUPPORT(prf) 1F468 200D 1F9BD ; fully-qualified # 👨‍🦽 man in manual wheelchair +#SUPPORT(prf) 1F468 1F3FB 200D 1F9BD ; fully-qualified # 👨🏻‍🦽 man in manual wheelchair: light skin tone +#SUPPORT(prf) 1F468 1F3FC 200D 1F9BD ; fully-qualified # 👨🏼‍🦽 man in manual wheelchair: medium-light skin tone +#SUPPORT(prf) 1F468 1F3FD 200D 1F9BD ; fully-qualified # 👨🏽‍🦽 man in manual wheelchair: medium skin tone +#SUPPORT(prf) 1F468 1F3FE 200D 1F9BD ; fully-qualified # 👨🏾‍🦽 man in manual wheelchair: medium-dark skin tone +#SUPPORT(prf) 1F468 1F3FF 200D 1F9BD ; fully-qualified # 👨🏿‍🦽 man in manual wheelchair: dark skin tone +#SUPPORT(prf) 1F469 200D 1F9BD ; fully-qualified # 👩‍🦽 woman in manual wheelchair +#SUPPORT(prf) 1F469 1F3FB 200D 1F9BD ; fully-qualified # 👩🏻‍🦽 woman in manual wheelchair: light skin tone +#SUPPORT(prf) 1F469 1F3FC 200D 1F9BD ; fully-qualified # 👩🏼‍🦽 woman in manual wheelchair: medium-light skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F9BD ; fully-qualified # 👩🏽‍🦽 woman in manual wheelchair: medium skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F9BD ; fully-qualified # 👩🏾‍🦽 woman in manual wheelchair: medium-dark skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F9BD ; fully-qualified # 👩🏿‍🦽 woman in manual wheelchair: dark skin tone +1F3C3 ; fully-qualified # 🏃 person running +1F3C3 1F3FB ; fully-qualified # 🏃🏻 person running: light skin tone +1F3C3 1F3FC ; fully-qualified # 🏃🏼 person running: medium-light skin tone +1F3C3 1F3FD ; fully-qualified # 🏃🏽 person running: medium skin tone +1F3C3 1F3FE ; fully-qualified # 🏃🏾 person running: medium-dark skin tone +1F3C3 1F3FF ; fully-qualified # 🏃🏿 person running: dark skin tone +#SUPPORT(prf) 1F3C3 200D 2642 FE0F ; fully-qualified # 🏃‍♂️ man running +#SUPPORT(prf) 1F3C3 1F3FB 200D 2642 FE0F ; fully-qualified # 🏃🏻‍♂️ man running: light skin tone +#SUPPORT(prf) 1F3C3 1F3FC 200D 2642 FE0F ; fully-qualified # 🏃🏼‍♂️ man running: medium-light skin tone +#SUPPORT(prf) 1F3C3 1F3FD 200D 2642 FE0F ; fully-qualified # 🏃🏽‍♂️ man running: medium skin tone +#SUPPORT(prf) 1F3C3 1F3FE 200D 2642 FE0F ; fully-qualified # 🏃🏾‍♂️ man running: medium-dark skin tone +#SUPPORT(prf) 1F3C3 1F3FF 200D 2642 FE0F ; fully-qualified # 🏃🏿‍♂️ man running: dark skin tone +#SUPPORT(prf) 1F3C3 200D 2640 FE0F ; fully-qualified # 🏃‍♀️ woman running +#SUPPORT(prf) 1F3C3 1F3FB 200D 2640 FE0F ; fully-qualified # 🏃🏻‍♀️ woman running: light skin tone +#SUPPORT(prf) 1F3C3 1F3FC 200D 2640 FE0F ; fully-qualified # 🏃🏼‍♀️ woman running: medium-light skin tone +#SUPPORT(prf) 1F3C3 1F3FD 200D 2640 FE0F ; fully-qualified # 🏃🏽‍♀️ woman running: medium skin tone +#SUPPORT(prf) 1F3C3 1F3FE 200D 2640 FE0F ; fully-qualified # 🏃🏾‍♀️ woman running: medium-dark skin tone +#SUPPORT(prf) 1F3C3 1F3FF 200D 2640 FE0F ; fully-qualified # 🏃🏿‍♀️ woman running: dark skin tone +1F483 ; fully-qualified # 💃 woman dancing +1F483 1F3FB ; fully-qualified # 💃🏻 woman dancing: light skin tone +1F483 1F3FC ; fully-qualified # 💃🏼 woman dancing: medium-light skin tone +1F483 1F3FD ; fully-qualified # 💃🏽 woman dancing: medium skin tone +1F483 1F3FE ; fully-qualified # 💃🏾 woman dancing: medium-dark skin tone +1F483 1F3FF ; fully-qualified # 💃🏿 woman dancing: dark skin tone +1F57A ; fully-qualified # 🕺 man dancing +1F57A 1F3FB ; fully-qualified # 🕺🏻 man dancing: light skin tone +1F57A 1F3FC ; fully-qualified # 🕺🏼 man dancing: medium-light skin tone +1F57A 1F3FD ; fully-qualified # 🕺🏽 man dancing: medium skin tone +1F57A 1F3FE ; fully-qualified # 🕺🏾 man dancing: medium-dark skin tone +1F57A 1F3FF ; fully-qualified # 🕺🏿 man dancing: dark skin tone +1F574 FE0F ; fully-qualified # 🕴️ man in suit levitating +1F574 1F3FB ; fully-qualified # 🕴🏻 man in suit levitating: light skin tone +1F574 1F3FC ; fully-qualified # 🕴🏼 man in suit levitating: medium-light skin tone +1F574 1F3FD ; fully-qualified # 🕴🏽 man in suit levitating: medium skin tone +1F574 1F3FE ; fully-qualified # 🕴🏾 man in suit levitating: medium-dark skin tone +1F574 1F3FF ; fully-qualified # 🕴🏿 man in suit levitating: dark skin tone +1F46F ; fully-qualified # 👯 people with bunny ears +#SUPPORT(prf) 1F46F 200D 2642 FE0F ; fully-qualified # 👯‍♂️ men with bunny ears +#SUPPORT(prf) 1F46F 200D 2640 FE0F ; fully-qualified # 👯‍♀️ women with bunny ears +1F9D6 ; fully-qualified # 🧖 person in steamy room +1F9D6 1F3FB ; fully-qualified # 🧖🏻 person in steamy room: light skin tone +1F9D6 1F3FC ; fully-qualified # 🧖🏼 person in steamy room: medium-light skin tone +1F9D6 1F3FD ; fully-qualified # 🧖🏽 person in steamy room: medium skin tone +1F9D6 1F3FE ; fully-qualified # 🧖🏾 person in steamy room: medium-dark skin tone +1F9D6 1F3FF ; fully-qualified # 🧖🏿 person in steamy room: dark skin tone +#SUPPORT(prf) 1F9D6 200D 2642 FE0F ; fully-qualified # 🧖‍♂️ man in steamy room +#SUPPORT(prf) 1F9D6 1F3FB 200D 2642 FE0F ; fully-qualified # 🧖🏻‍♂️ man in steamy room: light skin tone +#SUPPORT(prf) 1F9D6 1F3FC 200D 2642 FE0F ; fully-qualified # 🧖🏼‍♂️ man in steamy room: medium-light skin tone +#SUPPORT(prf) 1F9D6 1F3FD 200D 2642 FE0F ; fully-qualified # 🧖🏽‍♂️ man in steamy room: medium skin tone +#SUPPORT(prf) 1F9D6 1F3FE 200D 2642 FE0F ; fully-qualified # 🧖🏾‍♂️ man in steamy room: medium-dark skin tone +#SUPPORT(prf) 1F9D6 1F3FF 200D 2642 FE0F ; fully-qualified # 🧖🏿‍♂️ man in steamy room: dark skin tone +#SUPPORT(prf) 1F9D6 200D 2640 FE0F ; fully-qualified # 🧖‍♀️ woman in steamy room +#SUPPORT(prf) 1F9D6 1F3FB 200D 2640 FE0F ; fully-qualified # 🧖🏻‍♀️ woman in steamy room: light skin tone +#SUPPORT(prf) 1F9D6 1F3FC 200D 2640 FE0F ; fully-qualified # 🧖🏼‍♀️ woman in steamy room: medium-light skin tone +#SUPPORT(prf) 1F9D6 1F3FD 200D 2640 FE0F ; fully-qualified # 🧖🏽‍♀️ woman in steamy room: medium skin tone +#SUPPORT(prf) 1F9D6 1F3FE 200D 2640 FE0F ; fully-qualified # 🧖🏾‍♀️ woman in steamy room: medium-dark skin tone +#SUPPORT(prf) 1F9D6 1F3FF 200D 2640 FE0F ; fully-qualified # 🧖🏿‍♀️ woman in steamy room: dark skin tone +1F9D7 ; fully-qualified # 🧗 person climbing +1F9D7 1F3FB ; fully-qualified # 🧗🏻 person climbing: light skin tone +1F9D7 1F3FC ; fully-qualified # 🧗🏼 person climbing: medium-light skin tone +1F9D7 1F3FD ; fully-qualified # 🧗🏽 person climbing: medium skin tone +1F9D7 1F3FE ; fully-qualified # 🧗🏾 person climbing: medium-dark skin tone +1F9D7 1F3FF ; fully-qualified # 🧗🏿 person climbing: dark skin tone +#SUPPORT(prf) 1F9D7 200D 2642 FE0F ; fully-qualified # 🧗‍♂️ man climbing +#SUPPORT(prf) 1F9D7 1F3FB 200D 2642 FE0F ; fully-qualified # 🧗🏻‍♂️ man climbing: light skin tone +#SUPPORT(prf) 1F9D7 1F3FC 200D 2642 FE0F ; fully-qualified # 🧗🏼‍♂️ man climbing: medium-light skin tone +#SUPPORT(prf) 1F9D7 1F3FD 200D 2642 FE0F ; fully-qualified # 🧗🏽‍♂️ man climbing: medium skin tone +#SUPPORT(prf) 1F9D7 1F3FE 200D 2642 FE0F ; fully-qualified # 🧗🏾‍♂️ man climbing: medium-dark skin tone +#SUPPORT(prf) 1F9D7 1F3FF 200D 2642 FE0F ; fully-qualified # 🧗🏿‍♂️ man climbing: dark skin tone +#SUPPORT(prf) 1F9D7 200D 2640 FE0F ; fully-qualified # 🧗‍♀️ woman climbing +#SUPPORT(prf) 1F9D7 1F3FB 200D 2640 FE0F ; fully-qualified # 🧗🏻‍♀️ woman climbing: light skin tone +#SUPPORT(prf) 1F9D7 1F3FC 200D 2640 FE0F ; fully-qualified # 🧗🏼‍♀️ woman climbing: medium-light skin tone +#SUPPORT(prf) 1F9D7 1F3FD 200D 2640 FE0F ; fully-qualified # 🧗🏽‍♀️ woman climbing: medium skin tone +#SUPPORT(prf) 1F9D7 1F3FE 200D 2640 FE0F ; fully-qualified # 🧗🏾‍♀️ woman climbing: medium-dark skin tone +#SUPPORT(prf) 1F9D7 1F3FF 200D 2640 FE0F ; fully-qualified # 🧗🏿‍♀️ woman climbing: dark skin tone + +# subgroup: person-sport +1F93A ; fully-qualified # 🤺 person fencing +1F3C7 ; fully-qualified # 🏇 horse racing +1F3C7 1F3FB ; fully-qualified # 🏇🏻 horse racing: light skin tone +1F3C7 1F3FC ; fully-qualified # 🏇🏼 horse racing: medium-light skin tone +1F3C7 1F3FD ; fully-qualified # 🏇🏽 horse racing: medium skin tone +1F3C7 1F3FE ; fully-qualified # 🏇🏾 horse racing: medium-dark skin tone +1F3C7 1F3FF ; fully-qualified # 🏇🏿 horse racing: dark skin tone +26F7 FE0F ; fully-qualified # ⛷️ skier +1F3C2 ; fully-qualified # 🏂 snowboarder +1F3C2 1F3FB ; fully-qualified # 🏂🏻 snowboarder: light skin tone +1F3C2 1F3FC ; fully-qualified # 🏂🏼 snowboarder: medium-light skin tone +1F3C2 1F3FD ; fully-qualified # 🏂🏽 snowboarder: medium skin tone +1F3C2 1F3FE ; fully-qualified # 🏂🏾 snowboarder: medium-dark skin tone +1F3C2 1F3FF ; fully-qualified # 🏂🏿 snowboarder: dark skin tone +1F3CC FE0F ; fully-qualified # 🏌️ person golfing +1F3CC 1F3FB ; fully-qualified # 🏌🏻 person golfing: light skin tone +1F3CC 1F3FC ; fully-qualified # 🏌🏼 person golfing: medium-light skin tone +1F3CC 1F3FD ; fully-qualified # 🏌🏽 person golfing: medium skin tone +1F3CC 1F3FE ; fully-qualified # 🏌🏾 person golfing: medium-dark skin tone +1F3CC 1F3FF ; fully-qualified # 🏌🏿 person golfing: dark skin tone +#SUPPORT(prf) 1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️‍♂️ man golfing +#SUPPORT(prf) 1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻‍♂️ man golfing: light skin tone +#SUPPORT(prf) 1F3CC 1F3FC 200D 2642 FE0F ; fully-qualified # 🏌🏼‍♂️ man golfing: medium-light skin tone +#SUPPORT(prf) 1F3CC 1F3FD 200D 2642 FE0F ; fully-qualified # 🏌🏽‍♂️ man golfing: medium skin tone +#SUPPORT(prf) 1F3CC 1F3FE 200D 2642 FE0F ; fully-qualified # 🏌🏾‍♂️ man golfing: medium-dark skin tone +#SUPPORT(prf) 1F3CC 1F3FF 200D 2642 FE0F ; fully-qualified # 🏌🏿‍♂️ man golfing: dark skin tone +#SUPPORT(prf) 1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️‍♀️ woman golfing +#SUPPORT(prf) 1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻‍♀️ woman golfing: light skin tone +#SUPPORT(prf) 1F3CC 1F3FC 200D 2640 FE0F ; fully-qualified # 🏌🏼‍♀️ woman golfing: medium-light skin tone +#SUPPORT(prf) 1F3CC 1F3FD 200D 2640 FE0F ; fully-qualified # 🏌🏽‍♀️ woman golfing: medium skin tone +#SUPPORT(prf) 1F3CC 1F3FE 200D 2640 FE0F ; fully-qualified # 🏌🏾‍♀️ woman golfing: medium-dark skin tone +#SUPPORT(prf) 1F3CC 1F3FF 200D 2640 FE0F ; fully-qualified # 🏌🏿‍♀️ woman golfing: dark skin tone +1F3C4 ; fully-qualified # 🏄 person surfing +1F3C4 1F3FB ; fully-qualified # 🏄🏻 person surfing: light skin tone +1F3C4 1F3FC ; fully-qualified # 🏄🏼 person surfing: medium-light skin tone +1F3C4 1F3FD ; fully-qualified # 🏄🏽 person surfing: medium skin tone +1F3C4 1F3FE ; fully-qualified # 🏄🏾 person surfing: medium-dark skin tone +1F3C4 1F3FF ; fully-qualified # 🏄🏿 person surfing: dark skin tone +#SUPPORT(prf) 1F3C4 200D 2642 FE0F ; fully-qualified # 🏄‍♂️ man surfing +#SUPPORT(prf) 1F3C4 1F3FB 200D 2642 FE0F ; fully-qualified # 🏄🏻‍♂️ man surfing: light skin tone +#SUPPORT(prf) 1F3C4 1F3FC 200D 2642 FE0F ; fully-qualified # 🏄🏼‍♂️ man surfing: medium-light skin tone +#SUPPORT(prf) 1F3C4 1F3FD 200D 2642 FE0F ; fully-qualified # 🏄🏽‍♂️ man surfing: medium skin tone +#SUPPORT(prf) 1F3C4 1F3FE 200D 2642 FE0F ; fully-qualified # 🏄🏾‍♂️ man surfing: medium-dark skin tone +#SUPPORT(prf) 1F3C4 1F3FF 200D 2642 FE0F ; fully-qualified # 🏄🏿‍♂️ man surfing: dark skin tone +#SUPPORT(prf) 1F3C4 200D 2640 FE0F ; fully-qualified # 🏄‍♀️ woman surfing +#SUPPORT(prf) 1F3C4 1F3FB 200D 2640 FE0F ; fully-qualified # 🏄🏻‍♀️ woman surfing: light skin tone +#SUPPORT(prf) 1F3C4 1F3FC 200D 2640 FE0F ; fully-qualified # 🏄🏼‍♀️ woman surfing: medium-light skin tone +#SUPPORT(prf) 1F3C4 1F3FD 200D 2640 FE0F ; fully-qualified # 🏄🏽‍♀️ woman surfing: medium skin tone +#SUPPORT(prf) 1F3C4 1F3FE 200D 2640 FE0F ; fully-qualified # 🏄🏾‍♀️ woman surfing: medium-dark skin tone +#SUPPORT(prf) 1F3C4 1F3FF 200D 2640 FE0F ; fully-qualified # 🏄🏿‍♀️ woman surfing: dark skin tone +1F6A3 ; fully-qualified # 🚣 person rowing boat +1F6A3 1F3FB ; fully-qualified # 🚣🏻 person rowing boat: light skin tone +1F6A3 1F3FC ; fully-qualified # 🚣🏼 person rowing boat: medium-light skin tone +1F6A3 1F3FD ; fully-qualified # 🚣🏽 person rowing boat: medium skin tone +1F6A3 1F3FE ; fully-qualified # 🚣🏾 person rowing boat: medium-dark skin tone +1F6A3 1F3FF ; fully-qualified # 🚣🏿 person rowing boat: dark skin tone +#SUPPORT(prf) 1F6A3 200D 2642 FE0F ; fully-qualified # 🚣‍♂️ man rowing boat +#SUPPORT(prf) 1F6A3 1F3FB 200D 2642 FE0F ; fully-qualified # 🚣🏻‍♂️ man rowing boat: light skin tone +#SUPPORT(prf) 1F6A3 1F3FC 200D 2642 FE0F ; fully-qualified # 🚣🏼‍♂️ man rowing boat: medium-light skin tone +#SUPPORT(prf) 1F6A3 1F3FD 200D 2642 FE0F ; fully-qualified # 🚣🏽‍♂️ man rowing boat: medium skin tone +#SUPPORT(prf) 1F6A3 1F3FE 200D 2642 FE0F ; fully-qualified # 🚣🏾‍♂️ man rowing boat: medium-dark skin tone +#SUPPORT(prf) 1F6A3 1F3FF 200D 2642 FE0F ; fully-qualified # 🚣🏿‍♂️ man rowing boat: dark skin tone +#SUPPORT(prf) 1F6A3 200D 2640 FE0F ; fully-qualified # 🚣‍♀️ woman rowing boat +#SUPPORT(prf) 1F6A3 1F3FB 200D 2640 FE0F ; fully-qualified # 🚣🏻‍♀️ woman rowing boat: light skin tone +#SUPPORT(prf) 1F6A3 1F3FC 200D 2640 FE0F ; fully-qualified # 🚣🏼‍♀️ woman rowing boat: medium-light skin tone +#SUPPORT(prf) 1F6A3 1F3FD 200D 2640 FE0F ; fully-qualified # 🚣🏽‍♀️ woman rowing boat: medium skin tone +#SUPPORT(prf) 1F6A3 1F3FE 200D 2640 FE0F ; fully-qualified # 🚣🏾‍♀️ woman rowing boat: medium-dark skin tone +#SUPPORT(prf) 1F6A3 1F3FF 200D 2640 FE0F ; fully-qualified # 🚣🏿‍♀️ woman rowing boat: dark skin tone +1F3CA ; fully-qualified # 🏊 person swimming +1F3CA 1F3FB ; fully-qualified # 🏊🏻 person swimming: light skin tone +1F3CA 1F3FC ; fully-qualified # 🏊🏼 person swimming: medium-light skin tone +1F3CA 1F3FD ; fully-qualified # 🏊🏽 person swimming: medium skin tone +1F3CA 1F3FE ; fully-qualified # 🏊🏾 person swimming: medium-dark skin tone +1F3CA 1F3FF ; fully-qualified # 🏊🏿 person swimming: dark skin tone +#SUPPORT(prf) 1F3CA 200D 2642 FE0F ; fully-qualified # 🏊‍♂️ man swimming +#SUPPORT(prf) 1F3CA 1F3FB 200D 2642 FE0F ; fully-qualified # 🏊🏻‍♂️ man swimming: light skin tone +#SUPPORT(prf) 1F3CA 1F3FC 200D 2642 FE0F ; fully-qualified # 🏊🏼‍♂️ man swimming: medium-light skin tone +#SUPPORT(prf) 1F3CA 1F3FD 200D 2642 FE0F ; fully-qualified # 🏊🏽‍♂️ man swimming: medium skin tone +#SUPPORT(prf) 1F3CA 1F3FE 200D 2642 FE0F ; fully-qualified # 🏊🏾‍♂️ man swimming: medium-dark skin tone +#SUPPORT(prf) 1F3CA 1F3FF 200D 2642 FE0F ; fully-qualified # 🏊🏿‍♂️ man swimming: dark skin tone +#SUPPORT(prf) 1F3CA 200D 2640 FE0F ; fully-qualified # 🏊‍♀️ woman swimming +#SUPPORT(prf) 1F3CA 1F3FB 200D 2640 FE0F ; fully-qualified # 🏊🏻‍♀️ woman swimming: light skin tone +#SUPPORT(prf) 1F3CA 1F3FC 200D 2640 FE0F ; fully-qualified # 🏊🏼‍♀️ woman swimming: medium-light skin tone +#SUPPORT(prf) 1F3CA 1F3FD 200D 2640 FE0F ; fully-qualified # 🏊🏽‍♀️ woman swimming: medium skin tone +#SUPPORT(prf) 1F3CA 1F3FE 200D 2640 FE0F ; fully-qualified # 🏊🏾‍♀️ woman swimming: medium-dark skin tone +#SUPPORT(prf) 1F3CA 1F3FF 200D 2640 FE0F ; fully-qualified # 🏊🏿‍♀️ woman swimming: dark skin tone +26F9 FE0F ; fully-qualified # ⛹️ person bouncing ball +26F9 1F3FB ; fully-qualified # ⛹🏻 person bouncing ball: light skin tone +26F9 1F3FC ; fully-qualified # ⛹🏼 person bouncing ball: medium-light skin tone +26F9 1F3FD ; fully-qualified # ⛹🏽 person bouncing ball: medium skin tone +26F9 1F3FE ; fully-qualified # ⛹🏾 person bouncing ball: medium-dark skin tone +26F9 1F3FF ; fully-qualified # ⛹🏿 person bouncing ball: dark skin tone +#SUPPORT(prf) 26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️‍♂️ man bouncing ball +#SUPPORT(prf) 26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻‍♂️ man bouncing ball: light skin tone +#SUPPORT(prf) 26F9 1F3FC 200D 2642 FE0F ; fully-qualified # ⛹🏼‍♂️ man bouncing ball: medium-light skin tone +#SUPPORT(prf) 26F9 1F3FD 200D 2642 FE0F ; fully-qualified # ⛹🏽‍♂️ man bouncing ball: medium skin tone +#SUPPORT(prf) 26F9 1F3FE 200D 2642 FE0F ; fully-qualified # ⛹🏾‍♂️ man bouncing ball: medium-dark skin tone +#SUPPORT(prf) 26F9 1F3FF 200D 2642 FE0F ; fully-qualified # ⛹🏿‍♂️ man bouncing ball: dark skin tone +#SUPPORT(prf) 26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️‍♀️ woman bouncing ball +#SUPPORT(prf) 26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻‍♀️ woman bouncing ball: light skin tone +#SUPPORT(prf) 26F9 1F3FC 200D 2640 FE0F ; fully-qualified # ⛹🏼‍♀️ woman bouncing ball: medium-light skin tone +#SUPPORT(prf) 26F9 1F3FD 200D 2640 FE0F ; fully-qualified # ⛹🏽‍♀️ woman bouncing ball: medium skin tone +#SUPPORT(prf) 26F9 1F3FE 200D 2640 FE0F ; fully-qualified # ⛹🏾‍♀️ woman bouncing ball: medium-dark skin tone +#SUPPORT(prf) 26F9 1F3FF 200D 2640 FE0F ; fully-qualified # ⛹🏿‍♀️ woman bouncing ball: dark skin tone +1F3CB FE0F ; fully-qualified # 🏋️ person lifting weights +1F3CB 1F3FB ; fully-qualified # 🏋🏻 person lifting weights: light skin tone +1F3CB 1F3FC ; fully-qualified # 🏋🏼 person lifting weights: medium-light skin tone +1F3CB 1F3FD ; fully-qualified # 🏋🏽 person lifting weights: medium skin tone +1F3CB 1F3FE ; fully-qualified # 🏋🏾 person lifting weights: medium-dark skin tone +1F3CB 1F3FF ; fully-qualified # 🏋🏿 person lifting weights: dark skin tone +#SUPPORT(prf) 1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️‍♂️ man lifting weights +#SUPPORT(prf) 1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻‍♂️ man lifting weights: light skin tone +#SUPPORT(prf) 1F3CB 1F3FC 200D 2642 FE0F ; fully-qualified # 🏋🏼‍♂️ man lifting weights: medium-light skin tone +#SUPPORT(prf) 1F3CB 1F3FD 200D 2642 FE0F ; fully-qualified # 🏋🏽‍♂️ man lifting weights: medium skin tone +#SUPPORT(prf) 1F3CB 1F3FE 200D 2642 FE0F ; fully-qualified # 🏋🏾‍♂️ man lifting weights: medium-dark skin tone +#SUPPORT(prf) 1F3CB 1F3FF 200D 2642 FE0F ; fully-qualified # 🏋🏿‍♂️ man lifting weights: dark skin tone +#SUPPORT(prf) 1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️‍♀️ woman lifting weights +#SUPPORT(prf) 1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻‍♀️ woman lifting weights: light skin tone +#SUPPORT(prf) 1F3CB 1F3FC 200D 2640 FE0F ; fully-qualified # 🏋🏼‍♀️ woman lifting weights: medium-light skin tone +#SUPPORT(prf) 1F3CB 1F3FD 200D 2640 FE0F ; fully-qualified # 🏋🏽‍♀️ woman lifting weights: medium skin tone +#SUPPORT(prf) 1F3CB 1F3FE 200D 2640 FE0F ; fully-qualified # 🏋🏾‍♀️ woman lifting weights: medium-dark skin tone +#SUPPORT(prf) 1F3CB 1F3FF 200D 2640 FE0F ; fully-qualified # 🏋🏿‍♀️ woman lifting weights: dark skin tone +1F6B4 ; fully-qualified # 🚴 person biking +1F6B4 1F3FB ; fully-qualified # 🚴🏻 person biking: light skin tone +1F6B4 1F3FC ; fully-qualified # 🚴🏼 person biking: medium-light skin tone +1F6B4 1F3FD ; fully-qualified # 🚴🏽 person biking: medium skin tone +1F6B4 1F3FE ; fully-qualified # 🚴🏾 person biking: medium-dark skin tone +1F6B4 1F3FF ; fully-qualified # 🚴🏿 person biking: dark skin tone +#SUPPORT(prf) 1F6B4 200D 2642 FE0F ; fully-qualified # 🚴‍♂️ man biking +#SUPPORT(prf) 1F6B4 1F3FB 200D 2642 FE0F ; fully-qualified # 🚴🏻‍♂️ man biking: light skin tone +#SUPPORT(prf) 1F6B4 1F3FC 200D 2642 FE0F ; fully-qualified # 🚴🏼‍♂️ man biking: medium-light skin tone +#SUPPORT(prf) 1F6B4 1F3FD 200D 2642 FE0F ; fully-qualified # 🚴🏽‍♂️ man biking: medium skin tone +#SUPPORT(prf) 1F6B4 1F3FE 200D 2642 FE0F ; fully-qualified # 🚴🏾‍♂️ man biking: medium-dark skin tone +#SUPPORT(prf) 1F6B4 1F3FF 200D 2642 FE0F ; fully-qualified # 🚴🏿‍♂️ man biking: dark skin tone +#SUPPORT(prf) 1F6B4 200D 2640 FE0F ; fully-qualified # 🚴‍♀️ woman biking +#SUPPORT(prf) 1F6B4 1F3FB 200D 2640 FE0F ; fully-qualified # 🚴🏻‍♀️ woman biking: light skin tone +#SUPPORT(prf) 1F6B4 1F3FC 200D 2640 FE0F ; fully-qualified # 🚴🏼‍♀️ woman biking: medium-light skin tone +#SUPPORT(prf) 1F6B4 1F3FD 200D 2640 FE0F ; fully-qualified # 🚴🏽‍♀️ woman biking: medium skin tone +#SUPPORT(prf) 1F6B4 1F3FE 200D 2640 FE0F ; fully-qualified # 🚴🏾‍♀️ woman biking: medium-dark skin tone +#SUPPORT(prf) 1F6B4 1F3FF 200D 2640 FE0F ; fully-qualified # 🚴🏿‍♀️ woman biking: dark skin tone +1F6B5 ; fully-qualified # 🚵 person mountain biking +1F6B5 1F3FB ; fully-qualified # 🚵🏻 person mountain biking: light skin tone +1F6B5 1F3FC ; fully-qualified # 🚵🏼 person mountain biking: medium-light skin tone +1F6B5 1F3FD ; fully-qualified # 🚵🏽 person mountain biking: medium skin tone +1F6B5 1F3FE ; fully-qualified # 🚵🏾 person mountain biking: medium-dark skin tone +1F6B5 1F3FF ; fully-qualified # 🚵🏿 person mountain biking: dark skin tone +#SUPPORT(prf) 1F6B5 200D 2642 FE0F ; fully-qualified # 🚵‍♂️ man mountain biking +#SUPPORT(prf) 1F6B5 1F3FB 200D 2642 FE0F ; fully-qualified # 🚵🏻‍♂️ man mountain biking: light skin tone +#SUPPORT(prf) 1F6B5 1F3FC 200D 2642 FE0F ; fully-qualified # 🚵🏼‍♂️ man mountain biking: medium-light skin tone +#SUPPORT(prf) 1F6B5 1F3FD 200D 2642 FE0F ; fully-qualified # 🚵🏽‍♂️ man mountain biking: medium skin tone +#SUPPORT(prf) 1F6B5 1F3FE 200D 2642 FE0F ; fully-qualified # 🚵🏾‍♂️ man mountain biking: medium-dark skin tone +#SUPPORT(prf) 1F6B5 1F3FF 200D 2642 FE0F ; fully-qualified # 🚵🏿‍♂️ man mountain biking: dark skin tone +#SUPPORT(prf) 1F6B5 200D 2640 FE0F ; fully-qualified # 🚵‍♀️ woman mountain biking +#SUPPORT(prf) 1F6B5 1F3FB 200D 2640 FE0F ; fully-qualified # 🚵🏻‍♀️ woman mountain biking: light skin tone +#SUPPORT(prf) 1F6B5 1F3FC 200D 2640 FE0F ; fully-qualified # 🚵🏼‍♀️ woman mountain biking: medium-light skin tone +#SUPPORT(prf) 1F6B5 1F3FD 200D 2640 FE0F ; fully-qualified # 🚵🏽‍♀️ woman mountain biking: medium skin tone +#SUPPORT(prf) 1F6B5 1F3FE 200D 2640 FE0F ; fully-qualified # 🚵🏾‍♀️ woman mountain biking: medium-dark skin tone +#SUPPORT(prf) 1F6B5 1F3FF 200D 2640 FE0F ; fully-qualified # 🚵🏿‍♀️ woman mountain biking: dark skin tone +1F938 ; fully-qualified # 🤸 person cartwheeling +1F938 1F3FB ; fully-qualified # 🤸🏻 person cartwheeling: light skin tone +1F938 1F3FC ; fully-qualified # 🤸🏼 person cartwheeling: medium-light skin tone +1F938 1F3FD ; fully-qualified # 🤸🏽 person cartwheeling: medium skin tone +1F938 1F3FE ; fully-qualified # 🤸🏾 person cartwheeling: medium-dark skin tone +1F938 1F3FF ; fully-qualified # 🤸🏿 person cartwheeling: dark skin tone +#SUPPORT(prf) 1F938 200D 2642 FE0F ; fully-qualified # 🤸‍♂️ man cartwheeling +#SUPPORT(prf) 1F938 1F3FB 200D 2642 FE0F ; fully-qualified # 🤸🏻‍♂️ man cartwheeling: light skin tone +#SUPPORT(prf) 1F938 1F3FC 200D 2642 FE0F ; fully-qualified # 🤸🏼‍♂️ man cartwheeling: medium-light skin tone +#SUPPORT(prf) 1F938 1F3FD 200D 2642 FE0F ; fully-qualified # 🤸🏽‍♂️ man cartwheeling: medium skin tone +#SUPPORT(prf) 1F938 1F3FE 200D 2642 FE0F ; fully-qualified # 🤸🏾‍♂️ man cartwheeling: medium-dark skin tone +#SUPPORT(prf) 1F938 1F3FF 200D 2642 FE0F ; fully-qualified # 🤸🏿‍♂️ man cartwheeling: dark skin tone +#SUPPORT(prf) 1F938 200D 2640 FE0F ; fully-qualified # 🤸‍♀️ woman cartwheeling +#SUPPORT(prf) 1F938 1F3FB 200D 2640 FE0F ; fully-qualified # 🤸🏻‍♀️ woman cartwheeling: light skin tone +#SUPPORT(prf) 1F938 1F3FC 200D 2640 FE0F ; fully-qualified # 🤸🏼‍♀️ woman cartwheeling: medium-light skin tone +#SUPPORT(prf) 1F938 1F3FD 200D 2640 FE0F ; fully-qualified # 🤸🏽‍♀️ woman cartwheeling: medium skin tone +#SUPPORT(prf) 1F938 1F3FE 200D 2640 FE0F ; fully-qualified # 🤸🏾‍♀️ woman cartwheeling: medium-dark skin tone +#SUPPORT(prf) 1F938 1F3FF 200D 2640 FE0F ; fully-qualified # 🤸🏿‍♀️ woman cartwheeling: dark skin tone +1F93C ; fully-qualified # 🤼 people wrestling +#SUPPORT(prf) 1F93C 200D 2642 FE0F ; fully-qualified # 🤼‍♂️ men wrestling +#SUPPORT(prf) 1F93C 200D 2640 FE0F ; fully-qualified # 🤼‍♀️ women wrestling +1F93D ; fully-qualified # 🤽 person playing water polo +1F93D 1F3FB ; fully-qualified # 🤽🏻 person playing water polo: light skin tone +1F93D 1F3FC ; fully-qualified # 🤽🏼 person playing water polo: medium-light skin tone +1F93D 1F3FD ; fully-qualified # 🤽🏽 person playing water polo: medium skin tone +1F93D 1F3FE ; fully-qualified # 🤽🏾 person playing water polo: medium-dark skin tone +1F93D 1F3FF ; fully-qualified # 🤽🏿 person playing water polo: dark skin tone +#SUPPORT(prf) 1F93D 200D 2642 FE0F ; fully-qualified # 🤽‍♂️ man playing water polo +#SUPPORT(prf) 1F93D 1F3FB 200D 2642 FE0F ; fully-qualified # 🤽🏻‍♂️ man playing water polo: light skin tone +#SUPPORT(prf) 1F93D 1F3FC 200D 2642 FE0F ; fully-qualified # 🤽🏼‍♂️ man playing water polo: medium-light skin tone +#SUPPORT(prf) 1F93D 1F3FD 200D 2642 FE0F ; fully-qualified # 🤽🏽‍♂️ man playing water polo: medium skin tone +#SUPPORT(prf) 1F93D 1F3FE 200D 2642 FE0F ; fully-qualified # 🤽🏾‍♂️ man playing water polo: medium-dark skin tone +#SUPPORT(prf) 1F93D 1F3FF 200D 2642 FE0F ; fully-qualified # 🤽🏿‍♂️ man playing water polo: dark skin tone +#SUPPORT(prf) 1F93D 200D 2640 FE0F ; fully-qualified # 🤽‍♀️ woman playing water polo +#SUPPORT(prf) 1F93D 1F3FB 200D 2640 FE0F ; fully-qualified # 🤽🏻‍♀️ woman playing water polo: light skin tone +#SUPPORT(prf) 1F93D 1F3FC 200D 2640 FE0F ; fully-qualified # 🤽🏼‍♀️ woman playing water polo: medium-light skin tone +#SUPPORT(prf) 1F93D 1F3FD 200D 2640 FE0F ; fully-qualified # 🤽🏽‍♀️ woman playing water polo: medium skin tone +#SUPPORT(prf) 1F93D 1F3FE 200D 2640 FE0F ; fully-qualified # 🤽🏾‍♀️ woman playing water polo: medium-dark skin tone +#SUPPORT(prf) 1F93D 1F3FF 200D 2640 FE0F ; fully-qualified # 🤽🏿‍♀️ woman playing water polo: dark skin tone +1F93E ; fully-qualified # 🤾 person playing handball +1F93E 1F3FB ; fully-qualified # 🤾🏻 person playing handball: light skin tone +1F93E 1F3FC ; fully-qualified # 🤾🏼 person playing handball: medium-light skin tone +1F93E 1F3FD ; fully-qualified # 🤾🏽 person playing handball: medium skin tone +1F93E 1F3FE ; fully-qualified # 🤾🏾 person playing handball: medium-dark skin tone +1F93E 1F3FF ; fully-qualified # 🤾🏿 person playing handball: dark skin tone +#SUPPORT(prf) 1F93E 200D 2642 FE0F ; fully-qualified # 🤾‍♂️ man playing handball +#SUPPORT(prf) 1F93E 1F3FB 200D 2642 FE0F ; fully-qualified # 🤾🏻‍♂️ man playing handball: light skin tone +#SUPPORT(prf) 1F93E 1F3FC 200D 2642 FE0F ; fully-qualified # 🤾🏼‍♂️ man playing handball: medium-light skin tone +#SUPPORT(prf) 1F93E 1F3FD 200D 2642 FE0F ; fully-qualified # 🤾🏽‍♂️ man playing handball: medium skin tone +#SUPPORT(prf) 1F93E 1F3FE 200D 2642 FE0F ; fully-qualified # 🤾🏾‍♂️ man playing handball: medium-dark skin tone +#SUPPORT(prf) 1F93E 1F3FF 200D 2642 FE0F ; fully-qualified # 🤾🏿‍♂️ man playing handball: dark skin tone +#SUPPORT(prf) 1F93E 200D 2640 FE0F ; fully-qualified # 🤾‍♀️ woman playing handball +#SUPPORT(prf) 1F93E 1F3FB 200D 2640 FE0F ; fully-qualified # 🤾🏻‍♀️ woman playing handball: light skin tone +#SUPPORT(prf) 1F93E 1F3FC 200D 2640 FE0F ; fully-qualified # 🤾🏼‍♀️ woman playing handball: medium-light skin tone +#SUPPORT(prf) 1F93E 1F3FD 200D 2640 FE0F ; fully-qualified # 🤾🏽‍♀️ woman playing handball: medium skin tone +#SUPPORT(prf) 1F93E 1F3FE 200D 2640 FE0F ; fully-qualified # 🤾🏾‍♀️ woman playing handball: medium-dark skin tone +#SUPPORT(prf) 1F93E 1F3FF 200D 2640 FE0F ; fully-qualified # 🤾🏿‍♀️ woman playing handball: dark skin tone +1F939 ; fully-qualified # 🤹 person juggling +1F939 1F3FB ; fully-qualified # 🤹🏻 person juggling: light skin tone +1F939 1F3FC ; fully-qualified # 🤹🏼 person juggling: medium-light skin tone +1F939 1F3FD ; fully-qualified # 🤹🏽 person juggling: medium skin tone +1F939 1F3FE ; fully-qualified # 🤹🏾 person juggling: medium-dark skin tone +1F939 1F3FF ; fully-qualified # 🤹🏿 person juggling: dark skin tone +#SUPPORT(prf) 1F939 200D 2642 FE0F ; fully-qualified # 🤹‍♂️ man juggling +#SUPPORT(prf) 1F939 1F3FB 200D 2642 FE0F ; fully-qualified # 🤹🏻‍♂️ man juggling: light skin tone +#SUPPORT(prf) 1F939 1F3FC 200D 2642 FE0F ; fully-qualified # 🤹🏼‍♂️ man juggling: medium-light skin tone +#SUPPORT(prf) 1F939 1F3FD 200D 2642 FE0F ; fully-qualified # 🤹🏽‍♂️ man juggling: medium skin tone +#SUPPORT(prf) 1F939 1F3FE 200D 2642 FE0F ; fully-qualified # 🤹🏾‍♂️ man juggling: medium-dark skin tone +#SUPPORT(prf) 1F939 1F3FF 200D 2642 FE0F ; fully-qualified # 🤹🏿‍♂️ man juggling: dark skin tone +#SUPPORT(prf) 1F939 200D 2640 FE0F ; fully-qualified # 🤹‍♀️ woman juggling +#SUPPORT(prf) 1F939 1F3FB 200D 2640 FE0F ; fully-qualified # 🤹🏻‍♀️ woman juggling: light skin tone +#SUPPORT(prf) 1F939 1F3FC 200D 2640 FE0F ; fully-qualified # 🤹🏼‍♀️ woman juggling: medium-light skin tone +#SUPPORT(prf) 1F939 1F3FD 200D 2640 FE0F ; fully-qualified # 🤹🏽‍♀️ woman juggling: medium skin tone +#SUPPORT(prf) 1F939 1F3FE 200D 2640 FE0F ; fully-qualified # 🤹🏾‍♀️ woman juggling: medium-dark skin tone +#SUPPORT(prf) 1F939 1F3FF 200D 2640 FE0F ; fully-qualified # 🤹🏿‍♀️ woman juggling: dark skin tone + +# subgroup: person-resting +1F9D8 ; fully-qualified # 🧘 person in lotus position +1F9D8 1F3FB ; fully-qualified # 🧘🏻 person in lotus position: light skin tone +1F9D8 1F3FC ; fully-qualified # 🧘🏼 person in lotus position: medium-light skin tone +1F9D8 1F3FD ; fully-qualified # 🧘🏽 person in lotus position: medium skin tone +1F9D8 1F3FE ; fully-qualified # 🧘🏾 person in lotus position: medium-dark skin tone +1F9D8 1F3FF ; fully-qualified # 🧘🏿 person in lotus position: dark skin tone +#SUPPORT(prf) 1F9D8 200D 2642 FE0F ; fully-qualified # 🧘‍♂️ man in lotus position +#SUPPORT(prf) 1F9D8 1F3FB 200D 2642 FE0F ; fully-qualified # 🧘🏻‍♂️ man in lotus position: light skin tone +#SUPPORT(prf) 1F9D8 1F3FC 200D 2642 FE0F ; fully-qualified # 🧘🏼‍♂️ man in lotus position: medium-light skin tone +#SUPPORT(prf) 1F9D8 1F3FD 200D 2642 FE0F ; fully-qualified # 🧘🏽‍♂️ man in lotus position: medium skin tone +#SUPPORT(prf) 1F9D8 1F3FE 200D 2642 FE0F ; fully-qualified # 🧘🏾‍♂️ man in lotus position: medium-dark skin tone +#SUPPORT(prf) 1F9D8 1F3FF 200D 2642 FE0F ; fully-qualified # 🧘🏿‍♂️ man in lotus position: dark skin tone +#SUPPORT(prf) 1F9D8 200D 2640 FE0F ; fully-qualified # 🧘‍♀️ woman in lotus position +#SUPPORT(prf) 1F9D8 1F3FB 200D 2640 FE0F ; fully-qualified # 🧘🏻‍♀️ woman in lotus position: light skin tone +#SUPPORT(prf) 1F9D8 1F3FC 200D 2640 FE0F ; fully-qualified # 🧘🏼‍♀️ woman in lotus position: medium-light skin tone +#SUPPORT(prf) 1F9D8 1F3FD 200D 2640 FE0F ; fully-qualified # 🧘🏽‍♀️ woman in lotus position: medium skin tone +#SUPPORT(prf) 1F9D8 1F3FE 200D 2640 FE0F ; fully-qualified # 🧘🏾‍♀️ woman in lotus position: medium-dark skin tone +#SUPPORT(prf) 1F9D8 1F3FF 200D 2640 FE0F ; fully-qualified # 🧘🏿‍♀️ woman in lotus position: dark skin tone +1F6C0 ; fully-qualified # 🛀 person taking bath +1F6C0 1F3FB ; fully-qualified # 🛀🏻 person taking bath: light skin tone +1F6C0 1F3FC ; fully-qualified # 🛀🏼 person taking bath: medium-light skin tone +1F6C0 1F3FD ; fully-qualified # 🛀🏽 person taking bath: medium skin tone +1F6C0 1F3FE ; fully-qualified # 🛀🏾 person taking bath: medium-dark skin tone +1F6C0 1F3FF ; fully-qualified # 🛀🏿 person taking bath: dark skin tone +1F6CC ; fully-qualified # 🛌 person in bed +1F6CC 1F3FB ; fully-qualified # 🛌🏻 person in bed: light skin tone +1F6CC 1F3FC ; fully-qualified # 🛌🏼 person in bed: medium-light skin tone +1F6CC 1F3FD ; fully-qualified # 🛌🏽 person in bed: medium skin tone +1F6CC 1F3FE ; fully-qualified # 🛌🏾 person in bed: medium-dark skin tone +1F6CC 1F3FF ; fully-qualified # 🛌🏿 person in bed: dark skin tone + +# subgroup: family +#SUPPORT(prf) 1F9D1 200D 1F91D 200D 1F9D1 ; fully-qualified # 🧑‍🤝‍🧑 people holding hands +#SUPPORT(prf) 1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏻‍🤝‍🧑🏻 people holding hands: light skin tone +#SUPPORT(prf) 1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼‍🤝‍🧑🏻 people holding hands: medium-light skin tone, light skin tone +#SUPPORT(prf) 1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏼‍🤝‍🧑🏼 people holding hands: medium-light skin tone +#SUPPORT(prf) 1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽‍🤝‍🧑🏻 people holding hands: medium skin tone, light skin tone +#SUPPORT(prf) 1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽‍🤝‍🧑🏼 people holding hands: medium skin tone, medium-light skin tone +#SUPPORT(prf) 1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏽‍🤝‍🧑🏽 people holding hands: medium skin tone +#SUPPORT(prf) 1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾‍🤝‍🧑🏻 people holding hands: medium-dark skin tone, light skin tone +#SUPPORT(prf) 1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾‍🤝‍🧑🏼 people holding hands: medium-dark skin tone, medium-light skin tone +#SUPPORT(prf) 1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾‍🤝‍🧑🏽 people holding hands: medium-dark skin tone, medium skin tone +#SUPPORT(prf) 1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏾‍🤝‍🧑🏾 people holding hands: medium-dark skin tone +#SUPPORT(prf) 1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿‍🤝‍🧑🏻 people holding hands: dark skin tone, light skin tone +#SUPPORT(prf) 1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿‍🤝‍🧑🏼 people holding hands: dark skin tone, medium-light skin tone +#SUPPORT(prf) 1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿‍🤝‍🧑🏽 people holding hands: dark skin tone, medium skin tone +#SUPPORT(prf) 1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿‍🤝‍🧑🏾 people holding hands: dark skin tone, medium-dark skin tone +#SUPPORT(prf) 1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏿‍🤝‍🧑🏿 people holding hands: dark skin tone +1F46D ; fully-qualified # 👭 women holding hands +1F46D 1F3FB ; fully-qualified # 👭🏻 women holding hands: light skin tone +#SUPPORT(prf) 1F469 1F3FC 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏼‍🤝‍👩🏻 women holding hands: medium-light skin tone, light skin tone +1F46D 1F3FC ; fully-qualified # 👭🏼 women holding hands: medium-light skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏽‍🤝‍👩🏻 women holding hands: medium skin tone, light skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏽‍🤝‍👩🏼 women holding hands: medium skin tone, medium-light skin tone +1F46D 1F3FD ; fully-qualified # 👭🏽 women holding hands: medium skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏾‍🤝‍👩🏻 women holding hands: medium-dark skin tone, light skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏾‍🤝‍👩🏼 women holding hands: medium-dark skin tone, medium-light skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏾‍🤝‍👩🏽 women holding hands: medium-dark skin tone, medium skin tone +1F46D 1F3FE ; fully-qualified # 👭🏾 women holding hands: medium-dark skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏿‍🤝‍👩🏻 women holding hands: dark skin tone, light skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏿‍🤝‍👩🏼 women holding hands: dark skin tone, medium-light skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏿‍🤝‍👩🏽 women holding hands: dark skin tone, medium skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏿‍🤝‍👩🏾 women holding hands: dark skin tone, medium-dark skin tone +1F46D 1F3FF ; fully-qualified # 👭🏿 women holding hands: dark skin tone +1F46B ; fully-qualified # 👫 woman and man holding hands +1F46B 1F3FB ; fully-qualified # 👫🏻 woman and man holding hands: light skin tone +#SUPPORT(prf) 1F469 1F3FB 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏻‍🤝‍👨🏼 woman and man holding hands: light skin tone, medium-light skin tone +#SUPPORT(prf) 1F469 1F3FB 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏻‍🤝‍👨🏽 woman and man holding hands: light skin tone, medium skin tone +#SUPPORT(prf) 1F469 1F3FB 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏻‍🤝‍👨🏾 woman and man holding hands: light skin tone, medium-dark skin tone +#SUPPORT(prf) 1F469 1F3FB 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏻‍🤝‍👨🏿 woman and man holding hands: light skin tone, dark skin tone +#SUPPORT(prf) 1F469 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏼‍🤝‍👨🏻 woman and man holding hands: medium-light skin tone, light skin tone +1F46B 1F3FC ; fully-qualified # 👫🏼 woman and man holding hands: medium-light skin tone +#SUPPORT(prf) 1F469 1F3FC 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏼‍🤝‍👨🏽 woman and man holding hands: medium-light skin tone, medium skin tone +#SUPPORT(prf) 1F469 1F3FC 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏼‍🤝‍👨🏾 woman and man holding hands: medium-light skin tone, medium-dark skin tone +#SUPPORT(prf) 1F469 1F3FC 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏼‍🤝‍👨🏿 woman and man holding hands: medium-light skin tone, dark skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏽‍🤝‍👨🏻 woman and man holding hands: medium skin tone, light skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏽‍🤝‍👨🏼 woman and man holding hands: medium skin tone, medium-light skin tone +1F46B 1F3FD ; fully-qualified # 👫🏽 woman and man holding hands: medium skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏽‍🤝‍👨🏾 woman and man holding hands: medium skin tone, medium-dark skin tone +#SUPPORT(prf) 1F469 1F3FD 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏽‍🤝‍👨🏿 woman and man holding hands: medium skin tone, dark skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏾‍🤝‍👨🏻 woman and man holding hands: medium-dark skin tone, light skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏾‍🤝‍👨🏼 woman and man holding hands: medium-dark skin tone, medium-light skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏾‍🤝‍👨🏽 woman and man holding hands: medium-dark skin tone, medium skin tone +1F46B 1F3FE ; fully-qualified # 👫🏾 woman and man holding hands: medium-dark skin tone +#SUPPORT(prf) 1F469 1F3FE 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏾‍🤝‍👨🏿 woman and man holding hands: medium-dark skin tone, dark skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏿‍🤝‍👨🏻 woman and man holding hands: dark skin tone, light skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏿‍🤝‍👨🏼 woman and man holding hands: dark skin tone, medium-light skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏿‍🤝‍👨🏽 woman and man holding hands: dark skin tone, medium skin tone +#SUPPORT(prf) 1F469 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏿‍🤝‍👨🏾 woman and man holding hands: dark skin tone, medium-dark skin tone +1F46B 1F3FF ; fully-qualified # 👫🏿 woman and man holding hands: dark skin tone +1F46C ; fully-qualified # 👬 men holding hands +1F46C 1F3FB ; fully-qualified # 👬🏻 men holding hands: light skin tone +#SUPPORT(prf) 1F468 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏼‍🤝‍👨🏻 men holding hands: medium-light skin tone, light skin tone +1F46C 1F3FC ; fully-qualified # 👬🏼 men holding hands: medium-light skin tone +#SUPPORT(prf) 1F468 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏽‍🤝‍👨🏻 men holding hands: medium skin tone, light skin tone +#SUPPORT(prf) 1F468 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏽‍🤝‍👨🏼 men holding hands: medium skin tone, medium-light skin tone +1F46C 1F3FD ; fully-qualified # 👬🏽 men holding hands: medium skin tone +#SUPPORT(prf) 1F468 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏾‍🤝‍👨🏻 men holding hands: medium-dark skin tone, light skin tone +#SUPPORT(prf) 1F468 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏾‍🤝‍👨🏼 men holding hands: medium-dark skin tone, medium-light skin tone +#SUPPORT(prf) 1F468 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏾‍🤝‍👨🏽 men holding hands: medium-dark skin tone, medium skin tone +1F46C 1F3FE ; fully-qualified # 👬🏾 men holding hands: medium-dark skin tone +#SUPPORT(prf) 1F468 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏿‍🤝‍👨🏻 men holding hands: dark skin tone, light skin tone +#SUPPORT(prf) 1F468 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏿‍🤝‍👨🏼 men holding hands: dark skin tone, medium-light skin tone +#SUPPORT(prf) 1F468 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏿‍🤝‍👨🏽 men holding hands: dark skin tone, medium skin tone +#SUPPORT(prf) 1F468 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏿‍🤝‍👨🏾 men holding hands: dark skin tone, medium-dark skin tone +1F46C 1F3FF ; fully-qualified # 👬🏿 men holding hands: dark skin tone +1F48F ; fully-qualified # 💏 kiss +#SUPPORT(prf) 1F469 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👩‍❤️‍💋‍👨 kiss: woman, man +#SUPPORT(prf) 1F468 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👨‍❤️‍💋‍👨 kiss: man, man +#SUPPORT(prf) 1F469 200D 2764 FE0F 200D 1F48B 200D 1F469 ; fully-qualified # 👩‍❤️‍💋‍👩 kiss: woman, woman +#SUPPORT(prf) 1F491 ; fully-qualified # 💑 couple with heart +#SUPPORT(prf) 1F469 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👩‍❤️‍👨 couple with heart: woman, man +#SUPPORT(prf) 1F468 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👨‍❤️‍👨 couple with heart: man, man +#SUPPORT(prf) 1F469 200D 2764 FE0F 200D 1F469 ; fully-qualified # 👩‍❤️‍👩 couple with heart: woman, woman +1F46A ; fully-qualified # 👪 family +1F468 200D 1F469 200D 1F466 ; fully-qualified # 👨‍👩‍👦 family: man, woman, boy +1F468 200D 1F469 200D 1F467 ; fully-qualified # 👨‍👩‍👧 family: man, woman, girl +1F468 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👩‍👧‍👦 family: man, woman, girl, boy +1F468 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👩‍👦‍👦 family: man, woman, boy, boy +1F468 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👩‍👧‍👧 family: man, woman, girl, girl +1F468 200D 1F468 200D 1F466 ; fully-qualified # 👨‍👨‍👦 family: man, man, boy +1F468 200D 1F468 200D 1F467 ; fully-qualified # 👨‍👨‍👧 family: man, man, girl +1F468 200D 1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👨‍👧‍👦 family: man, man, girl, boy +1F468 200D 1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👨‍👦‍👦 family: man, man, boy, boy +1F468 200D 1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👨‍👧‍👧 family: man, man, girl, girl +1F469 200D 1F469 200D 1F466 ; fully-qualified # 👩‍👩‍👦 family: woman, woman, boy +1F469 200D 1F469 200D 1F467 ; fully-qualified # 👩‍👩‍👧 family: woman, woman, girl +1F469 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩‍👩‍👧‍👦 family: woman, woman, girl, boy +1F469 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩‍👩‍👦‍👦 family: woman, woman, boy, boy +1F469 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩‍👩‍👧‍👧 family: woman, woman, girl, girl +1F468 200D 1F466 ; fully-qualified # 👨‍👦 family: man, boy +1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👦‍👦 family: man, boy, boy +1F468 200D 1F467 ; fully-qualified # 👨‍👧 family: man, girl +1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👧‍👦 family: man, girl, boy +1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👧‍👧 family: man, girl, girl +1F469 200D 1F466 ; fully-qualified # 👩‍👦 family: woman, boy +1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩‍👦‍👦 family: woman, boy, boy +1F469 200D 1F467 ; fully-qualified # 👩‍👧 family: woman, girl +1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩‍👧‍👦 family: woman, girl, boy +1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩‍👧‍👧 family: woman, girl, girl + +# subgroup: person-symbol +1F5E3 FE0F ; fully-qualified # 🗣️ speaking head +1F464 ; fully-qualified # 👤 bust in silhouette +1F465 ; fully-qualified # 👥 busts in silhouette +1F463 ; fully-qualified # 👣 footprints + +# People & Body subtotal: 2212 +# People & Body subtotal: 447 w/o modifiers + +# group: Component + +# subgroup: skin-tone +1F3FB ; component # 🏻 light skin tone +1F3FC ; component # 🏼 medium-light skin tone +1F3FD ; component # 🏽 medium skin tone +1F3FE ; component # 🏾 medium-dark skin tone +1F3FF ; component # 🏿 dark skin tone + +# subgroup: hair-style +1F9B0 ; component # 🦰 red hair +1F9B1 ; component # 🦱 curly hair +1F9B3 ; component # 🦳 white hair +1F9B2 ; component # 🦲 bald + +# Component subtotal: 9 +# Component subtotal: 4 w/o modifiers + +# group: Animals & Nature + +# subgroup: animal-mammal +1F435 ; fully-qualified # 🐵 monkey face +1F412 ; fully-qualified # 🐒 monkey +1F98D ; fully-qualified # 🦍 gorilla +#SUPPORT(prf) 1F9A7 ; fully-qualified # 🦧 orangutan +1F436 ; fully-qualified # 🐶 dog face +1F415 ; fully-qualified # 🐕 dog +#SUPPORT(prf) 1F9AE ; fully-qualified # 🦮 guide dog +#SUPPORT(prf) 1F415 200D 1F9BA ; fully-qualified # 🐕‍🦺 service dog +1F429 ; fully-qualified # 🐩 poodle +1F43A ; fully-qualified # 🐺 wolf +1F98A ; fully-qualified # 🦊 fox +1F99D ; fully-qualified # 🦝 raccoon +1F431 ; fully-qualified # 🐱 cat face +1F408 ; fully-qualified # 🐈 cat +1F981 ; fully-qualified # 🦁 lion +1F42F ; fully-qualified # 🐯 tiger face +1F405 ; fully-qualified # 🐅 tiger +1F406 ; fully-qualified # 🐆 leopard +1F434 ; fully-qualified # 🐴 horse face +1F40E ; fully-qualified # 🐎 horse +1F984 ; fully-qualified # 🦄 unicorn +1F993 ; fully-qualified # 🦓 zebra +1F98C ; fully-qualified # 🦌 deer +1F42E ; fully-qualified # 🐮 cow face +1F402 ; fully-qualified # 🐂 ox +1F403 ; fully-qualified # 🐃 water buffalo +1F404 ; fully-qualified # 🐄 cow +1F437 ; fully-qualified # 🐷 pig face +1F416 ; fully-qualified # 🐖 pig +1F417 ; fully-qualified # 🐗 boar +1F43D ; fully-qualified # 🐽 pig nose +1F40F ; fully-qualified # 🐏 ram +1F411 ; fully-qualified # 🐑 ewe +1F410 ; fully-qualified # 🐐 goat +1F42A ; fully-qualified # 🐪 camel +1F42B ; fully-qualified # 🐫 two-hump camel +1F999 ; fully-qualified # 🦙 llama +1F992 ; fully-qualified # 🦒 giraffe +1F418 ; fully-qualified # 🐘 elephant +1F98F ; fully-qualified # 🦏 rhinoceros +1F99B ; fully-qualified # 🦛 hippopotamus +1F42D ; fully-qualified # 🐭 mouse face +1F401 ; fully-qualified # 🐁 mouse +1F400 ; fully-qualified # 🐀 rat +1F439 ; fully-qualified # 🐹 hamster +1F430 ; fully-qualified # 🐰 rabbit face +1F407 ; fully-qualified # 🐇 rabbit +1F43F FE0F ; fully-qualified # 🐿️ chipmunk +1F994 ; fully-qualified # 🦔 hedgehog +1F987 ; fully-qualified # 🦇 bat +1F43B ; fully-qualified # 🐻 bear +1F428 ; fully-qualified # 🐨 koala +1F43C ; fully-qualified # 🐼 panda +#SUPPORT(prf) 1F9A5 ; fully-qualified # 🦥 sloth +#SUPPORT(prf) 1F9A6 ; fully-qualified # 🦦 otter +#SUPPORT(prf) 1F9A8 ; fully-qualified # 🦨 skunk +1F998 ; fully-qualified # 🦘 kangaroo +1F9A1 ; fully-qualified # 🦡 badger +1F43E ; fully-qualified # 🐾 paw prints + +# subgroup: animal-bird +1F983 ; fully-qualified # 🦃 turkey +1F414 ; fully-qualified # 🐔 chicken +1F413 ; fully-qualified # 🐓 rooster +1F423 ; fully-qualified # 🐣 hatching chick +1F424 ; fully-qualified # 🐤 baby chick +1F425 ; fully-qualified # 🐥 front-facing baby chick +1F426 ; fully-qualified # 🐦 bird +1F427 ; fully-qualified # 🐧 penguin +1F54A FE0F ; fully-qualified # 🕊️ dove +1F985 ; fully-qualified # 🦅 eagle +1F986 ; fully-qualified # 🦆 duck +1F9A2 ; fully-qualified # 🦢 swan +1F989 ; fully-qualified # 🦉 owl +#SUPPORT(prf) 1F9A9 ; fully-qualified # 🦩 flamingo +1F99A ; fully-qualified # 🦚 peacock +1F99C ; fully-qualified # 🦜 parrot + +# subgroup: animal-amphibian +1F438 ; fully-qualified # 🐸 frog + +# subgroup: animal-reptile +1F40A ; fully-qualified # 🐊 crocodile +1F422 ; fully-qualified # 🐢 turtle +1F98E ; fully-qualified # 🦎 lizard +1F40D ; fully-qualified # 🐍 snake +1F432 ; fully-qualified # 🐲 dragon face +1F409 ; fully-qualified # 🐉 dragon +1F995 ; fully-qualified # 🦕 sauropod +1F996 ; fully-qualified # 🦖 T-Rex + +# subgroup: animal-marine +1F433 ; fully-qualified # 🐳 spouting whale +1F40B ; fully-qualified # 🐋 whale +1F42C ; fully-qualified # 🐬 dolphin +1F41F ; fully-qualified # 🐟 fish +1F420 ; fully-qualified # 🐠 tropical fish +1F421 ; fully-qualified # 🐡 blowfish +1F988 ; fully-qualified # 🦈 shark +1F419 ; fully-qualified # 🐙 octopus +1F41A ; fully-qualified # 🐚 spiral shell + +# subgroup: animal-bug +1F40C ; fully-qualified # 🐌 snail +1F98B ; fully-qualified # 🦋 butterfly +1F41B ; fully-qualified # 🐛 bug +1F41C ; fully-qualified # 🐜 ant +1F41D ; fully-qualified # 🐝 honeybee +1F41E ; fully-qualified # 🐞 lady beetle +1F997 ; fully-qualified # 🦗 cricket +1F577 FE0F ; fully-qualified # 🕷️ spider +1F578 FE0F ; fully-qualified # 🕸️ spider web +1F982 ; fully-qualified # 🦂 scorpion +1F99F ; fully-qualified # 🦟 mosquito +1F9A0 ; fully-qualified # 🦠 microbe + +# subgroup: plant-flower +1F490 ; fully-qualified # 💐 bouquet +1F338 ; fully-qualified # 🌸 cherry blossom +1F4AE ; fully-qualified # 💮 white flower +1F3F5 FE0F ; fully-qualified # 🏵️ rosette +1F339 ; fully-qualified # 🌹 rose +1F940 ; fully-qualified # 🥀 wilted flower +1F33A ; fully-qualified # 🌺 hibiscus +1F33B ; fully-qualified # 🌻 sunflower +1F33C ; fully-qualified # 🌼 blossom +1F337 ; fully-qualified # 🌷 tulip + +# subgroup: plant-other +1F331 ; fully-qualified # 🌱 seedling +1F332 ; fully-qualified # 🌲 evergreen tree +1F333 ; fully-qualified # 🌳 deciduous tree +1F334 ; fully-qualified # 🌴 palm tree +1F335 ; fully-qualified # 🌵 cactus +1F33E ; fully-qualified # 🌾 sheaf of rice +1F33F ; fully-qualified # 🌿 herb +2618 FE0F ; fully-qualified # ☘️ shamrock +1F340 ; fully-qualified # 🍀 four leaf clover +1F341 ; fully-qualified # 🍁 maple leaf +1F342 ; fully-qualified # 🍂 fallen leaf +1F343 ; fully-qualified # 🍃 leaf fluttering in wind + +# Animals & Nature subtotal: 133 +# Animals & Nature subtotal: 133 w/o modifiers + +# group: Food & Drink + +# subgroup: food-fruit +1F347 ; fully-qualified # 🍇 grapes +1F348 ; fully-qualified # 🍈 melon +1F349 ; fully-qualified # 🍉 watermelon +1F34A ; fully-qualified # 🍊 tangerine +1F34B ; fully-qualified # 🍋 lemon +1F34C ; fully-qualified # 🍌 banana +1F34D ; fully-qualified # 🍍 pineapple +1F96D ; fully-qualified # 🥭 mango +1F34E ; fully-qualified # 🍎 red apple +1F34F ; fully-qualified # 🍏 green apple +1F350 ; fully-qualified # 🍐 pear +1F351 ; fully-qualified # 🍑 peach +1F352 ; fully-qualified # 🍒 cherries +1F353 ; fully-qualified # 🍓 strawberry +1F95D ; fully-qualified # 🥝 kiwi fruit +1F345 ; fully-qualified # 🍅 tomato +1F965 ; fully-qualified # 🥥 coconut + +# subgroup: food-vegetable +1F951 ; fully-qualified # 🥑 avocado +1F346 ; fully-qualified # 🍆 eggplant +1F954 ; fully-qualified # 🥔 potato +1F955 ; fully-qualified # 🥕 carrot +1F33D ; fully-qualified # 🌽 ear of corn +1F336 FE0F ; fully-qualified # 🌶️ hot pepper +1F952 ; fully-qualified # 🥒 cucumber +1F96C ; fully-qualified # 🥬 leafy green +1F966 ; fully-qualified # 🥦 broccoli +#SUPPORT(prf) 1F9C4 ; fully-qualified # 🧄 garlic +#SUPPORT(prf) 1F9C5 ; fully-qualified # 🧅 onion +1F344 ; fully-qualified # 🍄 mushroom +1F95C ; fully-qualified # 🥜 peanuts +1F330 ; fully-qualified # 🌰 chestnut + +# subgroup: food-prepared +1F35E ; fully-qualified # 🍞 bread +1F950 ; fully-qualified # 🥐 croissant +1F956 ; fully-qualified # 🥖 baguette bread +1F968 ; fully-qualified # 🥨 pretzel +1F96F ; fully-qualified # 🥯 bagel +1F95E ; fully-qualified # 🥞 pancakes +#SUPPORT(prf) 1F9C7 ; fully-qualified # 🧇 waffle +1F9C0 ; fully-qualified # 🧀 cheese wedge +1F356 ; fully-qualified # 🍖 meat on bone +1F357 ; fully-qualified # 🍗 poultry leg +1F969 ; fully-qualified # 🥩 cut of meat +1F953 ; fully-qualified # 🥓 bacon +1F354 ; fully-qualified # 🍔 hamburger +1F35F ; fully-qualified # 🍟 french fries +1F355 ; fully-qualified # 🍕 pizza +1F32D ; fully-qualified # 🌭 hot dog +1F96A ; fully-qualified # 🥪 sandwich +1F32E ; fully-qualified # 🌮 taco +1F32F ; fully-qualified # 🌯 burrito +1F959 ; fully-qualified # 🥙 stuffed flatbread +#SUPPORT(prf) 1F9C6 ; fully-qualified # 🧆 falafel +1F95A ; fully-qualified # 🥚 egg +1F373 ; fully-qualified # 🍳 cooking +1F958 ; fully-qualified # 🥘 shallow pan of food +1F372 ; fully-qualified # 🍲 pot of food +1F963 ; fully-qualified # 🥣 bowl with spoon +1F957 ; fully-qualified # 🥗 green salad +1F37F ; fully-qualified # 🍿 popcorn +#SUPPORT(prf) 1F9C8 ; fully-qualified # 🧈 butter +1F9C2 ; fully-qualified # 🧂 salt +1F96B ; fully-qualified # 🥫 canned food + +# subgroup: food-asian +1F371 ; fully-qualified # 🍱 bento box +1F358 ; fully-qualified # 🍘 rice cracker +1F359 ; fully-qualified # 🍙 rice ball +1F35A ; fully-qualified # 🍚 cooked rice +1F35B ; fully-qualified # 🍛 curry rice +1F35C ; fully-qualified # 🍜 steaming bowl +1F35D ; fully-qualified # 🍝 spaghetti +1F360 ; fully-qualified # 🍠 roasted sweet potato +1F362 ; fully-qualified # 🍢 oden +1F363 ; fully-qualified # 🍣 sushi +1F364 ; fully-qualified # 🍤 fried shrimp +1F365 ; fully-qualified # 🍥 fish cake with swirl +1F96E ; fully-qualified # 🥮 moon cake +1F361 ; fully-qualified # 🍡 dango +1F95F ; fully-qualified # 🥟 dumpling +1F960 ; fully-qualified # 🥠 fortune cookie +1F961 ; fully-qualified # 🥡 takeout box + +# subgroup: food-marine +1F980 ; fully-qualified # 🦀 crab +1F99E ; fully-qualified # 🦞 lobster +1F990 ; fully-qualified # 🦐 shrimp +1F991 ; fully-qualified # 🦑 squid +#SUPPORT(prf) 1F9AA ; fully-qualified # 🦪 oyster + +# subgroup: food-sweet +1F366 ; fully-qualified # 🍦 soft ice cream +1F367 ; fully-qualified # 🍧 shaved ice +1F368 ; fully-qualified # 🍨 ice cream +1F369 ; fully-qualified # 🍩 doughnut +1F36A ; fully-qualified # 🍪 cookie +1F382 ; fully-qualified # 🎂 birthday cake +1F370 ; fully-qualified # 🍰 shortcake +1F9C1 ; fully-qualified # 🧁 cupcake +1F967 ; fully-qualified # 🥧 pie +1F36B ; fully-qualified # 🍫 chocolate bar +1F36C ; fully-qualified # 🍬 candy +1F36D ; fully-qualified # 🍭 lollipop +1F36E ; fully-qualified # 🍮 custard +1F36F ; fully-qualified # 🍯 honey pot + +# subgroup: drink +1F37C ; fully-qualified # 🍼 baby bottle +1F95B ; fully-qualified # 🥛 glass of milk +2615 ; fully-qualified # ☕ hot beverage +1F375 ; fully-qualified # 🍵 teacup without handle +1F376 ; fully-qualified # 🍶 sake +1F37E ; fully-qualified # 🍾 bottle with popping cork +1F377 ; fully-qualified # 🍷 wine glass +1F378 ; fully-qualified # 🍸 cocktail glass +1F379 ; fully-qualified # 🍹 tropical drink +1F37A ; fully-qualified # 🍺 beer mug +1F37B ; fully-qualified # 🍻 clinking beer mugs +1F942 ; fully-qualified # 🥂 clinking glasses +1F943 ; fully-qualified # 🥃 tumbler glass +1F964 ; fully-qualified # 🥤 cup with straw +#SUPPORT(prf) 1F9C3 ; fully-qualified # 🧃 beverage box +#SUPPORT(prf) 1F9C9 ; fully-qualified # 🧉 mate +#SUPPORT(prf) 1F9CA ; fully-qualified # 🧊 ice cube + +# subgroup: dishware +1F962 ; fully-qualified # 🥢 chopsticks +1F37D FE0F ; fully-qualified # 🍽️ fork and knife with plate +1F374 ; fully-qualified # 🍴 fork and knife +1F944 ; fully-qualified # 🥄 spoon +1F52A ; fully-qualified # 🔪 kitchen knife +1F3FA ; fully-qualified # 🏺 amphora + +# Food & Drink subtotal: 123 +# Food & Drink subtotal: 123 w/o modifiers + +# group: Travel & Places + +# subgroup: place-map +1F30D ; fully-qualified # 🌍 globe showing Europe-Africa +1F30E ; fully-qualified # 🌎 globe showing Americas +1F30F ; fully-qualified # 🌏 globe showing Asia-Australia +1F310 ; fully-qualified # 🌐 globe with meridians +1F5FA FE0F ; fully-qualified # 🗺️ world map +1F5FE ; fully-qualified # 🗾 map of Japan +1F9ED ; fully-qualified # 🧭 compass + +# subgroup: place-geographic +1F3D4 FE0F ; fully-qualified # 🏔️ snow-capped mountain +26F0 FE0F ; fully-qualified # ⛰️ mountain +1F30B ; fully-qualified # 🌋 volcano +1F5FB ; fully-qualified # 🗻 mount fuji +1F3D5 FE0F ; fully-qualified # 🏕️ camping +1F3D6 FE0F ; fully-qualified # 🏖️ beach with umbrella +1F3DC FE0F ; fully-qualified # 🏜️ desert +1F3DD FE0F ; fully-qualified # 🏝️ desert island +1F3DE FE0F ; fully-qualified # 🏞️ national park + +# subgroup: place-building +1F3DF FE0F ; fully-qualified # 🏟️ stadium +1F3DB FE0F ; fully-qualified # 🏛️ classical building +1F3D7 FE0F ; fully-qualified # 🏗️ building construction +1F9F1 ; fully-qualified # 🧱 brick +1F3D8 FE0F ; fully-qualified # 🏘️ houses +1F3DA FE0F ; fully-qualified # 🏚️ derelict house +1F3E0 ; fully-qualified # 🏠 house +1F3E1 ; fully-qualified # 🏡 house with garden +1F3E2 ; fully-qualified # 🏢 office building +1F3E3 ; fully-qualified # 🏣 Japanese post office +1F3E4 ; fully-qualified # 🏤 post office +1F3E5 ; fully-qualified # 🏥 hospital +1F3E6 ; fully-qualified # 🏦 bank +1F3E8 ; fully-qualified # 🏨 hotel +1F3E9 ; fully-qualified # 🏩 love hotel +1F3EA ; fully-qualified # 🏪 convenience store +1F3EB ; fully-qualified # 🏫 school +1F3EC ; fully-qualified # 🏬 department store +1F3ED ; fully-qualified # 🏭 factory +1F3EF ; fully-qualified # 🏯 Japanese castle +1F3F0 ; fully-qualified # 🏰 castle +1F492 ; fully-qualified # 💒 wedding +1F5FC ; fully-qualified # 🗼 Tokyo tower +1F5FD ; fully-qualified # 🗽 Statue of Liberty + +# subgroup: place-religious +26EA ; fully-qualified # ⛪ church +1F54C ; fully-qualified # 🕌 mosque +#SUPPORT(prf) 1F6D5 ; fully-qualified # 🛕 hindu temple +1F54D ; fully-qualified # 🕍 synagogue +26E9 FE0F ; fully-qualified # ⛩️ shinto shrine +1F54B ; fully-qualified # 🕋 kaaba + +# subgroup: place-other +26F2 ; fully-qualified # ⛲ fountain +26FA ; fully-qualified # ⛺ tent +1F301 ; fully-qualified # 🌁 foggy +1F303 ; fully-qualified # 🌃 night with stars +1F3D9 FE0F ; fully-qualified # 🏙️ cityscape +1F304 ; fully-qualified # 🌄 sunrise over mountains +1F305 ; fully-qualified # 🌅 sunrise +1F306 ; fully-qualified # 🌆 cityscape at dusk +1F307 ; fully-qualified # 🌇 sunset +1F309 ; fully-qualified # 🌉 bridge at night +2668 FE0F ; fully-qualified # ♨️ hot springs +1F3A0 ; fully-qualified # 🎠 carousel horse +1F3A1 ; fully-qualified # 🎡 ferris wheel +1F3A2 ; fully-qualified # 🎢 roller coaster +1F488 ; fully-qualified # 💈 barber pole +1F3AA ; fully-qualified # 🎪 circus tent + +# subgroup: transport-ground +1F682 ; fully-qualified # 🚂 locomotive +1F683 ; fully-qualified # 🚃 railway car +1F684 ; fully-qualified # 🚄 high-speed train +1F685 ; fully-qualified # 🚅 bullet train +1F686 ; fully-qualified # 🚆 train +1F687 ; fully-qualified # 🚇 metro +1F688 ; fully-qualified # 🚈 light rail +1F689 ; fully-qualified # 🚉 station +1F68A ; fully-qualified # 🚊 tram +1F69D ; fully-qualified # 🚝 monorail +1F69E ; fully-qualified # 🚞 mountain railway +1F68B ; fully-qualified # 🚋 tram car +1F68C ; fully-qualified # 🚌 bus +1F68D ; fully-qualified # 🚍 oncoming bus +1F68E ; fully-qualified # 🚎 trolleybus +1F690 ; fully-qualified # 🚐 minibus +1F691 ; fully-qualified # 🚑 ambulance +1F692 ; fully-qualified # 🚒 fire engine +1F693 ; fully-qualified # 🚓 police car +1F694 ; fully-qualified # 🚔 oncoming police car +1F695 ; fully-qualified # 🚕 taxi +1F696 ; fully-qualified # 🚖 oncoming taxi +1F697 ; fully-qualified # 🚗 automobile +1F698 ; fully-qualified # 🚘 oncoming automobile +1F699 ; fully-qualified # 🚙 sport utility vehicle +1F69A ; fully-qualified # 🚚 delivery truck +1F69B ; fully-qualified # 🚛 articulated lorry +1F69C ; fully-qualified # 🚜 tractor +1F3CE FE0F ; fully-qualified # 🏎️ racing car +1F3CD FE0F ; fully-qualified # 🏍️ motorcycle +1F6F5 ; fully-qualified # 🛵 motor scooter +#SUPPORT(prf) 1F9BD ; fully-qualified # 🦽 manual wheelchair +#SUPPORT(prf) 1F9BC ; fully-qualified # 🦼 motorized wheelchair +#SUPPORT(prf) 1F6FA ; fully-qualified # 🛺 auto rickshaw +1F6B2 ; fully-qualified # 🚲 bicycle +1F6F4 ; fully-qualified # 🛴 kick scooter +1F6F9 ; fully-qualified # 🛹 skateboard +1F68F ; fully-qualified # 🚏 bus stop +1F6E3 FE0F ; fully-qualified # 🛣️ motorway +1F6E4 FE0F ; fully-qualified # 🛤️ railway track +1F6E2 FE0F ; fully-qualified # 🛢️ oil drum +26FD ; fully-qualified # ⛽ fuel pump +1F6A8 ; fully-qualified # 🚨 police car light +1F6A5 ; fully-qualified # 🚥 horizontal traffic light +1F6A6 ; fully-qualified # 🚦 vertical traffic light +1F6D1 ; fully-qualified # 🛑 stop sign +1F6A7 ; fully-qualified # 🚧 construction + +# subgroup: transport-water +2693 ; fully-qualified # ⚓ anchor +26F5 ; fully-qualified # ⛵ sailboat +1F6F6 ; fully-qualified # 🛶 canoe +1F6A4 ; fully-qualified # 🚤 speedboat +1F6F3 FE0F ; fully-qualified # 🛳️ passenger ship +26F4 FE0F ; fully-qualified # ⛴️ ferry +1F6E5 FE0F ; fully-qualified # 🛥️ motor boat +1F6A2 ; fully-qualified # 🚢 ship + +# subgroup: transport-air +2708 FE0F ; fully-qualified # ✈️ airplane +1F6E9 FE0F ; fully-qualified # 🛩️ small airplane +1F6EB ; fully-qualified # 🛫 airplane departure +1F6EC ; fully-qualified # 🛬 airplane arrival +#SUPPORT(prf) 1FA82 ; fully-qualified # 🪂 parachute +1F4BA ; fully-qualified # 💺 seat +1F681 ; fully-qualified # 🚁 helicopter +1F69F ; fully-qualified # 🚟 suspension railway +1F6A0 ; fully-qualified # 🚠 mountain cableway +1F6A1 ; fully-qualified # 🚡 aerial tramway +1F6F0 FE0F ; fully-qualified # 🛰️ satellite +1F680 ; fully-qualified # 🚀 rocket +1F6F8 ; fully-qualified # 🛸 flying saucer + +# subgroup: hotel +1F6CE FE0F ; fully-qualified # 🛎️ bellhop bell +1F9F3 ; fully-qualified # 🧳 luggage + +# subgroup: time +231B ; fully-qualified # ⌛ hourglass done +23F3 ; fully-qualified # ⏳ hourglass not done +231A ; fully-qualified # ⌚ watch +23F0 ; fully-qualified # ⏰ alarm clock +23F1 FE0F ; fully-qualified # ⏱️ stopwatch +23F2 FE0F ; fully-qualified # ⏲️ timer clock +1F570 FE0F ; fully-qualified # 🕰️ mantelpiece clock +1F55B ; fully-qualified # 🕛 twelve o’clock +1F567 ; fully-qualified # 🕧 twelve-thirty +1F550 ; fully-qualified # 🕐 one o’clock +1F55C ; fully-qualified # 🕜 one-thirty +1F551 ; fully-qualified # 🕑 two o’clock +1F55D ; fully-qualified # 🕝 two-thirty +1F552 ; fully-qualified # 🕒 three o’clock +1F55E ; fully-qualified # 🕞 three-thirty +1F553 ; fully-qualified # 🕓 four o’clock +1F55F ; fully-qualified # 🕟 four-thirty +1F554 ; fully-qualified # 🕔 five o’clock +1F560 ; fully-qualified # 🕠 five-thirty +1F555 ; fully-qualified # 🕕 six o’clock +1F561 ; fully-qualified # 🕡 six-thirty +1F556 ; fully-qualified # 🕖 seven o’clock +1F562 ; fully-qualified # 🕢 seven-thirty +1F557 ; fully-qualified # 🕗 eight o’clock +1F563 ; fully-qualified # 🕣 eight-thirty +1F558 ; fully-qualified # 🕘 nine o’clock +1F564 ; fully-qualified # 🕤 nine-thirty +1F559 ; fully-qualified # 🕙 ten o’clock +1F565 ; fully-qualified # 🕥 ten-thirty +1F55A ; fully-qualified # 🕚 eleven o’clock +1F566 ; fully-qualified # 🕦 eleven-thirty + +# subgroup: sky & weather +1F311 ; fully-qualified # 🌑 new moon +1F312 ; fully-qualified # 🌒 waxing crescent moon +1F313 ; fully-qualified # 🌓 first quarter moon +1F314 ; fully-qualified # 🌔 waxing gibbous moon +1F315 ; fully-qualified # 🌕 full moon +1F316 ; fully-qualified # 🌖 waning gibbous moon +1F317 ; fully-qualified # 🌗 last quarter moon +1F318 ; fully-qualified # 🌘 waning crescent moon +1F319 ; fully-qualified # 🌙 crescent moon +1F31A ; fully-qualified # 🌚 new moon face +1F31B ; fully-qualified # 🌛 first quarter moon face +1F31C ; fully-qualified # 🌜 last quarter moon face +1F321 FE0F ; fully-qualified # 🌡️ thermometer +2600 FE0F ; fully-qualified # ☀️ sun +1F31D ; fully-qualified # 🌝 full moon face +1F31E ; fully-qualified # 🌞 sun with face +#SUPPORT(prf) 1FA90 ; fully-qualified # 🪐 ringed planet +2B50 ; fully-qualified # ⭐ star +1F31F ; fully-qualified # 🌟 glowing star +1F320 ; fully-qualified # 🌠 shooting star +1F30C ; fully-qualified # 🌌 milky way +2601 FE0F ; fully-qualified # ☁️ cloud +26C5 ; fully-qualified # ⛅ sun behind cloud +26C8 FE0F ; fully-qualified # ⛈️ cloud with lightning and rain +1F324 FE0F ; fully-qualified # 🌤️ sun behind small cloud +1F325 FE0F ; fully-qualified # 🌥️ sun behind large cloud +1F326 FE0F ; fully-qualified # 🌦️ sun behind rain cloud +1F327 FE0F ; fully-qualified # 🌧️ cloud with rain +1F328 FE0F ; fully-qualified # 🌨️ cloud with snow +1F329 FE0F ; fully-qualified # 🌩️ cloud with lightning +1F32A FE0F ; fully-qualified # 🌪️ tornado +1F32B FE0F ; fully-qualified # 🌫️ fog +1F32C FE0F ; fully-qualified # 🌬️ wind face +1F300 ; fully-qualified # 🌀 cyclone +1F308 ; fully-qualified # 🌈 rainbow +1F302 ; fully-qualified # 🌂 closed umbrella +2602 FE0F ; fully-qualified # ☂️ umbrella +2614 ; fully-qualified # ☔ umbrella with rain drops +26F1 FE0F ; fully-qualified # ⛱️ umbrella on ground +26A1 ; fully-qualified # ⚡ high voltage +2744 FE0F ; fully-qualified # ❄️ snowflake +2603 FE0F ; fully-qualified # ☃️ snowman +26C4 ; fully-qualified # ⛄ snowman without snow +2604 FE0F ; fully-qualified # ☄️ comet +1F525 ; fully-qualified # 🔥 fire +1F4A7 ; fully-qualified # 💧 droplet +1F30A ; fully-qualified # 🌊 water wave + +# Travel & Places subtotal: 259 +# Travel & Places subtotal: 259 w/o modifiers + +# group: Activities + +# subgroup: event +1F383 ; fully-qualified # 🎃 jack-o-lantern +1F384 ; fully-qualified # 🎄 Christmas tree +1F386 ; fully-qualified # 🎆 fireworks +1F387 ; fully-qualified # 🎇 sparkler +1F9E8 ; fully-qualified # 🧨 firecracker +2728 ; fully-qualified # ✨ sparkles +1F388 ; fully-qualified # 🎈 balloon +1F389 ; fully-qualified # 🎉 party popper +1F38A ; fully-qualified # 🎊 confetti ball +1F38B ; fully-qualified # 🎋 tanabata tree +1F38D ; fully-qualified # 🎍 pine decoration +1F38E ; fully-qualified # 🎎 Japanese dolls +1F38F ; fully-qualified # 🎏 carp streamer +1F390 ; fully-qualified # 🎐 wind chime +1F391 ; fully-qualified # 🎑 moon viewing ceremony +1F9E7 ; fully-qualified # 🧧 red envelope +1F380 ; fully-qualified # 🎀 ribbon +1F381 ; fully-qualified # 🎁 wrapped gift +1F397 FE0F ; fully-qualified # 🎗️ reminder ribbon +1F39F FE0F ; fully-qualified # 🎟️ admission tickets +1F3AB ; fully-qualified # 🎫 ticket + +# subgroup: award-medal +1F396 FE0F ; fully-qualified # 🎖️ military medal +1F3C6 ; fully-qualified # 🏆 trophy +1F3C5 ; fully-qualified # 🏅 sports medal +1F947 ; fully-qualified # 🥇 1st place medal +1F948 ; fully-qualified # 🥈 2nd place medal +1F949 ; fully-qualified # 🥉 3rd place medal + +# subgroup: sport +26BD ; fully-qualified # ⚽ soccer ball +26BE ; fully-qualified # ⚾ baseball +1F94E ; fully-qualified # 🥎 softball +1F3C0 ; fully-qualified # 🏀 basketball +1F3D0 ; fully-qualified # 🏐 volleyball +1F3C8 ; fully-qualified # 🏈 american football +1F3C9 ; fully-qualified # 🏉 rugby football +1F3BE ; fully-qualified # 🎾 tennis +1F94F ; fully-qualified # 🥏 flying disc +1F3B3 ; fully-qualified # 🎳 bowling +1F3CF ; fully-qualified # 🏏 cricket game +1F3D1 ; fully-qualified # 🏑 field hockey +1F3D2 ; fully-qualified # 🏒 ice hockey +1F94D ; fully-qualified # 🥍 lacrosse +1F3D3 ; fully-qualified # 🏓 ping pong +1F3F8 ; fully-qualified # 🏸 badminton +1F94A ; fully-qualified # 🥊 boxing glove +1F94B ; fully-qualified # 🥋 martial arts uniform +1F945 ; fully-qualified # 🥅 goal net +26F3 ; fully-qualified # ⛳ flag in hole +26F8 FE0F ; fully-qualified # ⛸️ ice skate +1F3A3 ; fully-qualified # 🎣 fishing pole +#SUPPORT(prf) 1F93F ; fully-qualified # 🤿 diving mask +1F3BD ; fully-qualified # 🎽 running shirt +1F3BF ; fully-qualified # 🎿 skis +1F6F7 ; fully-qualified # 🛷 sled +1F94C ; fully-qualified # 🥌 curling stone + +# subgroup: game +1F3AF ; fully-qualified # 🎯 direct hit +#SUPPORT(prf) 1FA80 ; fully-qualified # 🪀 yo-yo +#SUPPORT(prf) 1FA81 ; fully-qualified # 🪁 kite +1F3B1 ; fully-qualified # 🎱 pool 8 ball +1F52E ; fully-qualified # 🔮 crystal ball +1F9FF ; fully-qualified # 🧿 nazar amulet +1F3AE ; fully-qualified # 🎮 video game +1F579 FE0F ; fully-qualified # 🕹️ joystick +1F3B0 ; fully-qualified # 🎰 slot machine +1F3B2 ; fully-qualified # 🎲 game die +1F9E9 ; fully-qualified # 🧩 puzzle piece +1F9F8 ; fully-qualified # 🧸 teddy bear +2660 FE0F ; fully-qualified # ♠️ spade suit +2665 FE0F ; fully-qualified # ♥️ heart suit +2666 FE0F ; fully-qualified # ♦️ diamond suit +2663 FE0F ; fully-qualified # ♣️ club suit +265F FE0F ; fully-qualified # ♟️ chess pawn +1F0CF ; fully-qualified # 🃏 joker +1F004 ; fully-qualified # 🀄 mahjong red dragon +1F3B4 ; fully-qualified # 🎴 flower playing cards + +# subgroup: arts & crafts +1F3AD ; fully-qualified # 🎭 performing arts +1F5BC FE0F ; fully-qualified # 🖼️ framed picture +1F3A8 ; fully-qualified # 🎨 artist palette +1F9F5 ; fully-qualified # 🧵 thread +1F9F6 ; fully-qualified # 🧶 yarn + +# Activities subtotal: 90 +# Activities subtotal: 90 w/o modifiers + +# group: Objects + +# subgroup: clothing +1F453 ; fully-qualified # 👓 glasses +1F576 FE0F ; fully-qualified # 🕶️ sunglasses +1F97D ; fully-qualified # 🥽 goggles +1F97C ; fully-qualified # 🥼 lab coat +#SUPPORT(prf) 1F9BA ; fully-qualified # 🦺 safety vest +1F454 ; fully-qualified # 👔 necktie +1F455 ; fully-qualified # 👕 t-shirt +1F456 ; fully-qualified # 👖 jeans +1F9E3 ; fully-qualified # 🧣 scarf +1F9E4 ; fully-qualified # 🧤 gloves +1F9E5 ; fully-qualified # 🧥 coat +1F9E6 ; fully-qualified # 🧦 socks +1F457 ; fully-qualified # 👗 dress +1F458 ; fully-qualified # 👘 kimono +#SUPPORT(prf) 1F97B ; fully-qualified # 🥻 sari +#SUPPORT(prf) 1FA71 ; fully-qualified # 🩱 one-piece swimsuit +#SUPPORT(prf) 1FA72 ; fully-qualified # 🩲 swim brief +#SUPPORT(prf) 1FA73 ; fully-qualified # 🩳 shorts +1F459 ; fully-qualified # 👙 bikini +1F45A ; fully-qualified # 👚 woman’s clothes +1F45B ; fully-qualified # 👛 purse +1F45C ; fully-qualified # 👜 handbag +1F45D ; fully-qualified # 👝 clutch bag +1F6CD FE0F ; fully-qualified # 🛍️ shopping bags +1F392 ; fully-qualified # 🎒 backpack +1F45E ; fully-qualified # 👞 man’s shoe +1F45F ; fully-qualified # 👟 running shoe +1F97E ; fully-qualified # 🥾 hiking boot +1F97F ; fully-qualified # 🥿 flat shoe +1F460 ; fully-qualified # 👠 high-heeled shoe +1F461 ; fully-qualified # 👡 woman’s sandal +#SUPPORT(prf) 1FA70 ; fully-qualified # 🩰 ballet shoes +1F462 ; fully-qualified # 👢 woman’s boot +1F451 ; fully-qualified # 👑 crown +1F452 ; fully-qualified # 👒 woman’s hat +1F3A9 ; fully-qualified # 🎩 top hat +1F393 ; fully-qualified # 🎓 graduation cap +1F9E2 ; fully-qualified # 🧢 billed cap +26D1 FE0F ; fully-qualified # ⛑️ rescue worker’s helmet +1F4FF ; fully-qualified # 📿 prayer beads +1F484 ; fully-qualified # 💄 lipstick +1F48D ; fully-qualified # 💍 ring +1F48E ; fully-qualified # 💎 gem stone + +# subgroup: sound +1F507 ; fully-qualified # 🔇 muted speaker +1F508 ; fully-qualified # 🔈 speaker low volume +1F509 ; fully-qualified # 🔉 speaker medium volume +1F50A ; fully-qualified # 🔊 speaker high volume +1F4E2 ; fully-qualified # 📢 loudspeaker +1F4E3 ; fully-qualified # 📣 megaphone +1F4EF ; fully-qualified # 📯 postal horn +1F514 ; fully-qualified # 🔔 bell +1F515 ; fully-qualified # 🔕 bell with slash + +# subgroup: music +1F3BC ; fully-qualified # 🎼 musical score +1F3B5 ; fully-qualified # 🎵 musical note +1F3B6 ; fully-qualified # 🎶 musical notes +1F399 FE0F ; fully-qualified # 🎙️ studio microphone +1F39A FE0F ; fully-qualified # 🎚️ level slider +1F39B FE0F ; fully-qualified # 🎛️ control knobs +1F3A4 ; fully-qualified # 🎤 microphone +1F3A7 ; fully-qualified # 🎧 headphone +1F4FB ; fully-qualified # 📻 radio + +# subgroup: musical-instrument +1F3B7 ; fully-qualified # 🎷 saxophone +1F3B8 ; fully-qualified # 🎸 guitar +1F3B9 ; fully-qualified # 🎹 musical keyboard +1F3BA ; fully-qualified # 🎺 trumpet +1F3BB ; fully-qualified # 🎻 violin +#SUPPORT(prf) 1FA95 ; fully-qualified # 🪕 banjo +1F941 ; fully-qualified # 🥁 drum + +# subgroup: phone +1F4F1 ; fully-qualified # 📱 mobile phone +1F4F2 ; fully-qualified # 📲 mobile phone with arrow +260E FE0F ; fully-qualified # ☎️ telephone +1F4DE ; fully-qualified # 📞 telephone receiver +1F4DF ; fully-qualified # 📟 pager +1F4E0 ; fully-qualified # 📠 fax machine + +# subgroup: computer +1F50B ; fully-qualified # 🔋 battery +1F50C ; fully-qualified # 🔌 electric plug +1F4BB ; fully-qualified # 💻 laptop computer +1F5A5 FE0F ; fully-qualified # 🖥️ desktop computer +1F5A8 FE0F ; fully-qualified # 🖨️ printer +2328 FE0F ; fully-qualified # ⌨️ keyboard +1F5B1 FE0F ; fully-qualified # 🖱️ computer mouse +1F5B2 FE0F ; fully-qualified # 🖲️ trackball +1F4BD ; fully-qualified # 💽 computer disk +1F4BE ; fully-qualified # 💾 floppy disk +1F4BF ; fully-qualified # 💿 optical disk +1F4C0 ; fully-qualified # 📀 dvd +1F9EE ; fully-qualified # 🧮 abacus + +# subgroup: light & video +1F3A5 ; fully-qualified # 🎥 movie camera +1F39E FE0F ; fully-qualified # 🎞️ film frames +1F4FD FE0F ; fully-qualified # 📽️ film projector +1F3AC ; fully-qualified # 🎬 clapper board +1F4FA ; fully-qualified # 📺 television +1F4F7 ; fully-qualified # 📷 camera +1F4F8 ; fully-qualified # 📸 camera with flash +1F4F9 ; fully-qualified # 📹 video camera +1F4FC ; fully-qualified # 📼 videocassette +1F50D ; fully-qualified # 🔍 magnifying glass tilted left +1F50E ; fully-qualified # 🔎 magnifying glass tilted right +1F56F FE0F ; fully-qualified # 🕯️ candle +1F4A1 ; fully-qualified # 💡 light bulb +1F526 ; fully-qualified # 🔦 flashlight +1F3EE ; fully-qualified # 🏮 red paper lantern +#SUPPORT(prf) 1FA94 ; fully-qualified # 🪔 diya lamp + +# subgroup: book-paper +1F4D4 ; fully-qualified # 📔 notebook with decorative cover +1F4D5 ; fully-qualified # 📕 closed book +1F4D6 ; fully-qualified # 📖 open book +1F4D7 ; fully-qualified # 📗 green book +1F4D8 ; fully-qualified # 📘 blue book +1F4D9 ; fully-qualified # 📙 orange book +1F4DA ; fully-qualified # 📚 books +1F4D3 ; fully-qualified # 📓 notebook +1F4D2 ; fully-qualified # 📒 ledger +1F4C3 ; fully-qualified # 📃 page with curl +1F4DC ; fully-qualified # 📜 scroll +1F4C4 ; fully-qualified # 📄 page facing up +1F4F0 ; fully-qualified # 📰 newspaper +1F5DE FE0F ; fully-qualified # 🗞️ rolled-up newspaper +1F4D1 ; fully-qualified # 📑 bookmark tabs +1F516 ; fully-qualified # 🔖 bookmark +1F3F7 FE0F ; fully-qualified # 🏷️ label + +# subgroup: money +1F4B0 ; fully-qualified # 💰 money bag +1F4B4 ; fully-qualified # 💴 yen banknote +1F4B5 ; fully-qualified # 💵 dollar banknote +1F4B6 ; fully-qualified # 💶 euro banknote +1F4B7 ; fully-qualified # 💷 pound banknote +1F4B8 ; fully-qualified # 💸 money with wings +1F4B3 ; fully-qualified # 💳 credit card +1F9FE ; fully-qualified # 🧾 receipt +1F4B9 ; fully-qualified # 💹 chart increasing with yen +1F4B1 ; fully-qualified # 💱 currency exchange +1F4B2 ; fully-qualified # 💲 heavy dollar sign + +# subgroup: mail +2709 FE0F ; fully-qualified # ✉️ envelope +1F4E7 ; fully-qualified # 📧 e-mail +1F4E8 ; fully-qualified # 📨 incoming envelope +1F4E9 ; fully-qualified # 📩 envelope with arrow +1F4E4 ; fully-qualified # 📤 outbox tray +1F4E5 ; fully-qualified # 📥 inbox tray +1F4E6 ; fully-qualified # 📦 package +1F4EB ; fully-qualified # 📫 closed mailbox with raised flag +1F4EA ; fully-qualified # 📪 closed mailbox with lowered flag +1F4EC ; fully-qualified # 📬 open mailbox with raised flag +1F4ED ; fully-qualified # 📭 open mailbox with lowered flag +1F4EE ; fully-qualified # 📮 postbox +1F5F3 FE0F ; fully-qualified # 🗳️ ballot box with ballot + +# subgroup: writing +270F FE0F ; fully-qualified # ✏️ pencil +2712 FE0F ; fully-qualified # ✒️ black nib +1F58B FE0F ; fully-qualified # 🖋️ fountain pen +1F58A FE0F ; fully-qualified # 🖊️ pen +1F58C FE0F ; fully-qualified # 🖌️ paintbrush +1F58D FE0F ; fully-qualified # 🖍️ crayon +1F4DD ; fully-qualified # 📝 memo + +# subgroup: office +1F4BC ; fully-qualified # 💼 briefcase +1F4C1 ; fully-qualified # 📁 file folder +1F4C2 ; fully-qualified # 📂 open file folder +1F5C2 FE0F ; fully-qualified # 🗂️ card index dividers +1F4C5 ; fully-qualified # 📅 calendar +1F4C6 ; fully-qualified # 📆 tear-off calendar +1F5D2 FE0F ; fully-qualified # 🗒️ spiral notepad +1F5D3 FE0F ; fully-qualified # 🗓️ spiral calendar +1F4C7 ; fully-qualified # 📇 card index +1F4C8 ; fully-qualified # 📈 chart increasing +1F4C9 ; fully-qualified # 📉 chart decreasing +1F4CA ; fully-qualified # 📊 bar chart +1F4CB ; fully-qualified # 📋 clipboard +1F4CC ; fully-qualified # 📌 pushpin +1F4CD ; fully-qualified # 📍 round pushpin +1F4CE ; fully-qualified # 📎 paperclip +1F587 FE0F ; fully-qualified # 🖇️ linked paperclips +1F4CF ; fully-qualified # 📏 straight ruler +1F4D0 ; fully-qualified # 📐 triangular ruler +2702 FE0F ; fully-qualified # ✂️ scissors +1F5C3 FE0F ; fully-qualified # 🗃️ card file box +1F5C4 FE0F ; fully-qualified # 🗄️ file cabinet +1F5D1 FE0F ; fully-qualified # 🗑️ wastebasket + +# subgroup: lock +1F512 ; fully-qualified # 🔒 locked +1F513 ; fully-qualified # 🔓 unlocked +1F50F ; fully-qualified # 🔏 locked with pen +1F510 ; fully-qualified # 🔐 locked with key +1F511 ; fully-qualified # 🔑 key +1F5DD FE0F ; fully-qualified # 🗝️ old key + +# subgroup: tool +1F528 ; fully-qualified # 🔨 hammer +#SUPPORT(prf) 1FA93 ; fully-qualified # 🪓 axe +26CF FE0F ; fully-qualified # ⛏️ pick +2692 FE0F ; fully-qualified # ⚒️ hammer and pick +1F6E0 FE0F ; fully-qualified # 🛠️ hammer and wrench +1F5E1 FE0F ; fully-qualified # 🗡️ dagger +2694 FE0F ; fully-qualified # ⚔️ crossed swords +1F52B ; fully-qualified # 🔫 pistol +1F3F9 ; fully-qualified # 🏹 bow and arrow +1F6E1 FE0F ; fully-qualified # 🛡️ shield +1F527 ; fully-qualified # 🔧 wrench +1F529 ; fully-qualified # 🔩 nut and bolt +2699 FE0F ; fully-qualified # ⚙️ gear +1F5DC FE0F ; fully-qualified # 🗜️ clamp +2696 FE0F ; fully-qualified # ⚖️ balance scale +#SUPPORT(prf) 1F9AF ; fully-qualified # 🦯 probing cane +1F517 ; fully-qualified # 🔗 link +26D3 FE0F ; fully-qualified # ⛓️ chains +1F9F0 ; fully-qualified # 🧰 toolbox +1F9F2 ; fully-qualified # 🧲 magnet + +# subgroup: science +2697 FE0F ; fully-qualified # ⚗️ alembic +1F9EA ; fully-qualified # 🧪 test tube +1F9EB ; fully-qualified # 🧫 petri dish +1F9EC ; fully-qualified # 🧬 dna +1F52C ; fully-qualified # 🔬 microscope +1F52D ; fully-qualified # 🔭 telescope +1F4E1 ; fully-qualified # 📡 satellite antenna + +# subgroup: medical +1F489 ; fully-qualified # 💉 syringe +#SUPPORT(prf) 1FA78 ; fully-qualified # 🩸 drop of blood +1F48A ; fully-qualified # 💊 pill +#SUPPORT(prf) 1FA79 ; fully-qualified # 🩹 adhesive bandage +#SUPPORT(prf) 1FA7A ; fully-qualified # 🩺 stethoscope + +# subgroup: household +1F6AA ; fully-qualified # 🚪 door +1F6CF FE0F ; fully-qualified # 🛏️ bed +1F6CB FE0F ; fully-qualified # 🛋️ couch and lamp +#SUPPORT(prf) 1FA91 ; fully-qualified # 🪑 chair +1F6BD ; fully-qualified # 🚽 toilet +1F6BF ; fully-qualified # 🚿 shower +1F6C1 ; fully-qualified # 🛁 bathtub +#SUPPORT(prf) 1FA92 ; fully-qualified # 🪒 razor +1F9F4 ; fully-qualified # 🧴 lotion bottle +1F9F7 ; fully-qualified # 🧷 safety pin +1F9F9 ; fully-qualified # 🧹 broom +1F9FA ; fully-qualified # 🧺 basket +1F9FB ; fully-qualified # 🧻 roll of paper +1F9FC ; fully-qualified # 🧼 soap +1F9FD ; fully-qualified # 🧽 sponge +1F9EF ; fully-qualified # 🧯 fire extinguisher +1F6D2 ; fully-qualified # 🛒 shopping cart + +# subgroup: other-object +1F6AC ; fully-qualified # 🚬 cigarette +26B0 FE0F ; fully-qualified # ⚰️ coffin +26B1 FE0F ; fully-qualified # ⚱️ funeral urn +1F5FF ; fully-qualified # 🗿 moai + +# Objects subtotal: 282 +# Objects subtotal: 282 w/o modifiers + +# group: Symbols + +# subgroup: transport-sign +1F3E7 ; fully-qualified # 🏧 ATM sign +1F6AE ; fully-qualified # 🚮 litter in bin sign +1F6B0 ; fully-qualified # 🚰 potable water +267F ; fully-qualified # ♿ wheelchair symbol +1F6B9 ; fully-qualified # 🚹 men’s room +1F6BA ; fully-qualified # 🚺 women’s room +1F6BB ; fully-qualified # 🚻 restroom +1F6BC ; fully-qualified # 🚼 baby symbol +1F6BE ; fully-qualified # 🚾 water closet +1F6C2 ; fully-qualified # 🛂 passport control +1F6C3 ; fully-qualified # 🛃 customs +1F6C4 ; fully-qualified # 🛄 baggage claim +1F6C5 ; fully-qualified # 🛅 left luggage + +# subgroup: warning +26A0 FE0F ; fully-qualified # ⚠️ warning +1F6B8 ; fully-qualified # 🚸 children crossing +26D4 ; fully-qualified # ⛔ no entry +1F6AB ; fully-qualified # 🚫 prohibited +1F6B3 ; fully-qualified # 🚳 no bicycles +1F6AD ; fully-qualified # 🚭 no smoking +1F6AF ; fully-qualified # 🚯 no littering +1F6B1 ; fully-qualified # 🚱 non-potable water +1F6B7 ; fully-qualified # 🚷 no pedestrians +1F4F5 ; fully-qualified # 📵 no mobile phones +1F51E ; fully-qualified # 🔞 no one under eighteen +2622 FE0F ; fully-qualified # ☢️ radioactive +2623 FE0F ; fully-qualified # ☣️ biohazard + +# subgroup: arrow +2B06 FE0F ; fully-qualified # ⬆️ up arrow +2197 FE0F ; fully-qualified # ↗️ up-right arrow +27A1 FE0F ; fully-qualified # ➡️ right arrow +2198 FE0F ; fully-qualified # ↘️ down-right arrow +2B07 FE0F ; fully-qualified # ⬇️ down arrow +2199 FE0F ; fully-qualified # ↙️ down-left arrow +2B05 FE0F ; fully-qualified # ⬅️ left arrow +2196 FE0F ; fully-qualified # ↖️ up-left arrow +2195 FE0F ; fully-qualified # ↕️ up-down arrow +2194 FE0F ; fully-qualified # ↔️ left-right arrow +21A9 FE0F ; fully-qualified # ↩️ right arrow curving left +21AA FE0F ; fully-qualified # ↪️ left arrow curving right +2934 FE0F ; fully-qualified # ⤴️ right arrow curving up +2935 FE0F ; fully-qualified # ⤵️ right arrow curving down +1F503 ; fully-qualified # 🔃 clockwise vertical arrows +1F504 ; fully-qualified # 🔄 counterclockwise arrows button +1F519 ; fully-qualified # 🔙 BACK arrow +1F51A ; fully-qualified # 🔚 END arrow +1F51B ; fully-qualified # 🔛 ON! arrow +1F51C ; fully-qualified # 🔜 SOON arrow +1F51D ; fully-qualified # 🔝 TOP arrow + +# subgroup: religion +1F6D0 ; fully-qualified # 🛐 place of worship +269B FE0F ; fully-qualified # ⚛️ atom symbol +1F549 FE0F ; fully-qualified # 🕉️ om +2721 FE0F ; fully-qualified # ✡️ star of David +2638 FE0F ; fully-qualified # ☸️ wheel of dharma +262F FE0F ; fully-qualified # ☯️ yin yang +271D FE0F ; fully-qualified # ✝️ latin cross +2626 FE0F ; fully-qualified # ☦️ orthodox cross +262A FE0F ; fully-qualified # ☪️ star and crescent +262E FE0F ; fully-qualified # ☮️ peace symbol +1F54E ; fully-qualified # 🕎 menorah +1F52F ; fully-qualified # 🔯 dotted six-pointed star + +# subgroup: zodiac +2648 ; fully-qualified # ♈ Aries +2649 ; fully-qualified # ♉ Taurus +264A ; fully-qualified # ♊ Gemini +264B ; fully-qualified # ♋ Cancer +264C ; fully-qualified # ♌ Leo +264D ; fully-qualified # ♍ Virgo +264E ; fully-qualified # ♎ Libra +264F ; fully-qualified # ♏ Scorpio +2650 ; fully-qualified # ♐ Sagittarius +2651 ; fully-qualified # ♑ Capricorn +2652 ; fully-qualified # ♒ Aquarius +2653 ; fully-qualified # ♓ Pisces +26CE ; fully-qualified # ⛎ Ophiuchus + +# subgroup: av-symbol +1F500 ; fully-qualified # 🔀 shuffle tracks button +1F501 ; fully-qualified # 🔁 repeat button +1F502 ; fully-qualified # 🔂 repeat single button +25B6 FE0F ; fully-qualified # ▶️ play button +23E9 ; fully-qualified # ⏩ fast-forward button +23ED FE0F ; fully-qualified # ⏭️ next track button +23EF FE0F ; fully-qualified # ⏯️ play or pause button +25C0 FE0F ; fully-qualified # ◀️ reverse button +23EA ; fully-qualified # ⏪ fast reverse button +23EE FE0F ; fully-qualified # ⏮️ last track button +1F53C ; fully-qualified # 🔼 upwards button +23EB ; fully-qualified # ⏫ fast up button +1F53D ; fully-qualified # 🔽 downwards button +23EC ; fully-qualified # ⏬ fast down button +23F8 FE0F ; fully-qualified # ⏸️ pause button +23F9 FE0F ; fully-qualified # ⏹️ stop button +23FA FE0F ; fully-qualified # ⏺️ record button +23CF FE0F ; fully-qualified # ⏏️ eject button +1F3A6 ; fully-qualified # 🎦 cinema +1F505 ; fully-qualified # 🔅 dim button +1F506 ; fully-qualified # 🔆 bright button +1F4F6 ; fully-qualified # 📶 antenna bars +1F4F3 ; fully-qualified # 📳 vibration mode +1F4F4 ; fully-qualified # 📴 mobile phone off + +# subgroup: gender +2640 FE0F ; fully-qualified # ♀️ female sign +2642 FE0F ; fully-qualified # ♂️ male sign + +# subgroup: other-symbol +2695 FE0F ; fully-qualified # ⚕️ medical symbol +267E FE0F ; fully-qualified # ♾️ infinity +267B FE0F ; fully-qualified # ♻️ recycling symbol +269C FE0F ; fully-qualified # ⚜️ fleur-de-lis +1F531 ; fully-qualified # 🔱 trident emblem +1F4DB ; fully-qualified # 📛 name badge +1F530 ; fully-qualified # 🔰 Japanese symbol for beginner +2B55 ; fully-qualified # ⭕ hollow red circle +2705 ; fully-qualified # ✅ check mark button +2611 FE0F ; fully-qualified # ☑️ check box with check +2714 FE0F ; fully-qualified # ✔️ check mark +2716 FE0F ; fully-qualified # ✖️ multiplication sign +274C ; fully-qualified # ❌ cross mark +274E ; fully-qualified # ❎ cross mark button +2795 ; fully-qualified # ➕ plus sign +2796 ; fully-qualified # ➖ minus sign +2797 ; fully-qualified # ➗ division sign +27B0 ; fully-qualified # ➰ curly loop +27BF ; fully-qualified # ➿ double curly loop +303D FE0F ; fully-qualified # 〽️ part alternation mark +2733 FE0F ; fully-qualified # ✳️ eight-spoked asterisk +2734 FE0F ; fully-qualified # ✴️ eight-pointed star +2747 FE0F ; fully-qualified # ❇️ sparkle +203C FE0F ; fully-qualified # ‼️ double exclamation mark +2049 FE0F ; fully-qualified # ⁉️ exclamation question mark +2753 ; fully-qualified # ❓ question mark +2754 ; fully-qualified # ❔ white question mark +2755 ; fully-qualified # ❕ white exclamation mark +2757 ; fully-qualified # ❗ exclamation mark +3030 FE0F ; fully-qualified # 〰️ wavy dash +00A9 FE0F ; fully-qualified # ©️ copyright +00AE FE0F ; fully-qualified # ®️ registered +2122 FE0F ; fully-qualified # ™️ trade mark + +# subgroup: keycap +0023 FE0F 20E3 ; fully-qualified # #️⃣ keycap: # +002A FE0F 20E3 ; fully-qualified # *️⃣ keycap: * +0030 FE0F 20E3 ; fully-qualified # 0️⃣ keycap: 0 +0031 FE0F 20E3 ; fully-qualified # 1️⃣ keycap: 1 +0032 FE0F 20E3 ; fully-qualified # 2️⃣ keycap: 2 +0033 FE0F 20E3 ; fully-qualified # 3️⃣ keycap: 3 +0034 FE0F 20E3 ; fully-qualified # 4️⃣ keycap: 4 +0035 FE0F 20E3 ; fully-qualified # 5️⃣ keycap: 5 +0036 FE0F 20E3 ; fully-qualified # 6️⃣ keycap: 6 +0037 FE0F 20E3 ; fully-qualified # 7️⃣ keycap: 7 +0038 FE0F 20E3 ; fully-qualified # 8️⃣ keycap: 8 +0039 FE0F 20E3 ; fully-qualified # 9️⃣ keycap: 9 +1F51F ; fully-qualified # 🔟 keycap: 10 + +# subgroup: alphanum +1F520 ; fully-qualified # 🔠 input latin uppercase +1F521 ; fully-qualified # 🔡 input latin lowercase +1F522 ; fully-qualified # 🔢 input numbers +1F523 ; fully-qualified # 🔣 input symbols +1F524 ; fully-qualified # 🔤 input latin letters +1F170 FE0F ; fully-qualified # 🅰️ A button (blood type) +1F18E ; fully-qualified # 🆎 AB button (blood type) +1F171 FE0F ; fully-qualified # 🅱️ B button (blood type) +1F191 ; fully-qualified # 🆑 CL button +1F192 ; fully-qualified # 🆒 COOL button +1F193 ; fully-qualified # 🆓 FREE button +2139 FE0F ; fully-qualified # ℹ️ information +1F194 ; fully-qualified # 🆔 ID button +24C2 FE0F ; fully-qualified # Ⓜ️ circled M +1F195 ; fully-qualified # 🆕 NEW button +1F196 ; fully-qualified # 🆖 NG button +1F17E FE0F ; fully-qualified # 🅾️ O button (blood type) +1F197 ; fully-qualified # 🆗 OK button +1F17F FE0F ; fully-qualified # 🅿️ P button +1F198 ; fully-qualified # 🆘 SOS button +1F199 ; fully-qualified # 🆙 UP! button +1F19A ; fully-qualified # 🆚 VS button +1F201 ; fully-qualified # 🈁 Japanese “here” button +1F202 FE0F ; fully-qualified # 🈂️ Japanese “service charge” button +1F237 FE0F ; fully-qualified # 🈷️ Japanese “monthly amount” button +1F236 ; fully-qualified # 🈶 Japanese “not free of charge” button +1F22F ; fully-qualified # 🈯 Japanese “reserved” button +1F250 ; fully-qualified # 🉐 Japanese “bargain” button +1F239 ; fully-qualified # 🈹 Japanese “discount” button +1F21A ; fully-qualified # 🈚 Japanese “free of charge” button +1F232 ; fully-qualified # 🈲 Japanese “prohibited” button +1F251 ; fully-qualified # 🉑 Japanese “acceptable” button +1F238 ; fully-qualified # 🈸 Japanese “application” button +1F234 ; fully-qualified # 🈴 Japanese “passing grade” button +1F233 ; fully-qualified # 🈳 Japanese “vacancy” button +3297 FE0F ; fully-qualified # ㊗️ Japanese “congratulations” button +3299 FE0F ; fully-qualified # ㊙️ Japanese “secret” button +1F23A ; fully-qualified # 🈺 Japanese “open for business” button +1F235 ; fully-qualified # 🈵 Japanese “no vacancy” button + +# subgroup: geometric +1F534 ; fully-qualified # 🔴 red circle +#SUPPORT(prf) 1F7E0 ; fully-qualified # 🟠 orange circle +#SUPPORT(prf) 1F7E1 ; fully-qualified # 🟡 yellow circle +#SUPPORT(prf) 1F7E2 ; fully-qualified # 🟢 green circle +1F535 ; fully-qualified # 🔵 blue circle +#SUPPORT(prf) 1F7E3 ; fully-qualified # 🟣 purple circle +#SUPPORT(prf) 1F7E4 ; fully-qualified # 🟤 brown circle +26AB ; fully-qualified # ⚫ black circle +26AA ; fully-qualified # ⚪ white circle +#SUPPORT(prf) 1F7E5 ; fully-qualified # 🟥 red square +#SUPPORT(prf) 1F7E7 ; fully-qualified # 🟧 orange square +#SUPPORT(prf) 1F7E8 ; fully-qualified # 🟨 yellow square +#SUPPORT(prf) 1F7E9 ; fully-qualified # 🟩 green square +#SUPPORT(prf) 1F7E6 ; fully-qualified # 🟦 blue square +#SUPPORT(prf) 1F7EA ; fully-qualified # 🟪 purple square +#SUPPORT(prf) 1F7EB ; fully-qualified # 🟫 brown square +2B1B ; fully-qualified # ⬛ black large square +2B1C ; fully-qualified # ⬜ white large square +25FC FE0F ; fully-qualified # ◼️ black medium square +25FB FE0F ; fully-qualified # ◻️ white medium square +25FE ; fully-qualified # ◾ black medium-small square +25FD ; fully-qualified # ◽ white medium-small square +25AA FE0F ; fully-qualified # ▪️ black small square +25AB FE0F ; fully-qualified # ▫️ white small square +1F536 ; fully-qualified # 🔶 large orange diamond +1F537 ; fully-qualified # 🔷 large blue diamond +1F538 ; fully-qualified # 🔸 small orange diamond +1F539 ; fully-qualified # 🔹 small blue diamond +1F53A ; fully-qualified # 🔺 red triangle pointed up +1F53B ; fully-qualified # 🔻 red triangle pointed down +1F4A0 ; fully-qualified # 💠 diamond with a dot +1F518 ; fully-qualified # 🔘 radio button +1F533 ; fully-qualified # 🔳 white square button +1F532 ; fully-qualified # 🔲 black square button + +# Symbols subtotal: 297 +# Symbols subtotal: 297 w/o modifiers + +# group: Flags + +# subgroup: flag +1F3C1 ; fully-qualified # 🏁 chequered flag +1F6A9 ; fully-qualified # 🚩 triangular flag +1F38C ; fully-qualified # 🎌 crossed flags +1F3F4 ; fully-qualified # 🏴 black flag +1F3F3 FE0F ; fully-qualified # 🏳️ white flag +1F3F3 FE0F 200D 1F308 ; fully-qualified # 🏳️‍🌈 rainbow flag +#SUPPORT(prf) 1F3F4 200D 2620 FE0F ; fully-qualified # 🏴‍☠️ pirate flag + +# subgroup: country-flag +1F1E6 1F1E8 ; fully-qualified # 🇦🇨 flag: Ascension Island +1F1E6 1F1E9 ; fully-qualified # 🇦🇩 flag: Andorra +1F1E6 1F1EA ; fully-qualified # 🇦🇪 flag: United Arab Emirates +1F1E6 1F1EB ; fully-qualified # 🇦🇫 flag: Afghanistan +1F1E6 1F1EC ; fully-qualified # 🇦🇬 flag: Antigua & Barbuda +1F1E6 1F1EE ; fully-qualified # 🇦🇮 flag: Anguilla +1F1E6 1F1F1 ; fully-qualified # 🇦🇱 flag: Albania +1F1E6 1F1F2 ; fully-qualified # 🇦🇲 flag: Armenia +1F1E6 1F1F4 ; fully-qualified # 🇦🇴 flag: Angola +1F1E6 1F1F6 ; fully-qualified # 🇦🇶 flag: Antarctica +1F1E6 1F1F7 ; fully-qualified # 🇦🇷 flag: Argentina +1F1E6 1F1F8 ; fully-qualified # 🇦🇸 flag: American Samoa +1F1E6 1F1F9 ; fully-qualified # 🇦🇹 flag: Austria +1F1E6 1F1FA ; fully-qualified # 🇦🇺 flag: Australia +1F1E6 1F1FC ; fully-qualified # 🇦🇼 flag: Aruba +1F1E6 1F1FD ; fully-qualified # 🇦🇽 flag: Åland Islands +1F1E6 1F1FF ; fully-qualified # 🇦🇿 flag: Azerbaijan +1F1E7 1F1E6 ; fully-qualified # 🇧🇦 flag: Bosnia & Herzegovina +1F1E7 1F1E7 ; fully-qualified # 🇧🇧 flag: Barbados +1F1E7 1F1E9 ; fully-qualified # 🇧🇩 flag: Bangladesh +1F1E7 1F1EA ; fully-qualified # 🇧🇪 flag: Belgium +1F1E7 1F1EB ; fully-qualified # 🇧🇫 flag: Burkina Faso +1F1E7 1F1EC ; fully-qualified # 🇧🇬 flag: Bulgaria +1F1E7 1F1ED ; fully-qualified # 🇧🇭 flag: Bahrain +1F1E7 1F1EE ; fully-qualified # 🇧🇮 flag: Burundi +1F1E7 1F1EF ; fully-qualified # 🇧🇯 flag: Benin +1F1E7 1F1F1 ; fully-qualified # 🇧🇱 flag: St. Barthélemy +1F1E7 1F1F2 ; fully-qualified # 🇧🇲 flag: Bermuda +1F1E7 1F1F3 ; fully-qualified # 🇧🇳 flag: Brunei +1F1E7 1F1F4 ; fully-qualified # 🇧🇴 flag: Bolivia +1F1E7 1F1F6 ; fully-qualified # 🇧🇶 flag: Caribbean Netherlands +1F1E7 1F1F7 ; fully-qualified # 🇧🇷 flag: Brazil +1F1E7 1F1F8 ; fully-qualified # 🇧🇸 flag: Bahamas +1F1E7 1F1F9 ; fully-qualified # 🇧🇹 flag: Bhutan +1F1E7 1F1FB ; fully-qualified # 🇧🇻 flag: Bouvet Island +1F1E7 1F1FC ; fully-qualified # 🇧🇼 flag: Botswana +1F1E7 1F1FE ; fully-qualified # 🇧🇾 flag: Belarus +1F1E7 1F1FF ; fully-qualified # 🇧🇿 flag: Belize +1F1E8 1F1E6 ; fully-qualified # 🇨🇦 flag: Canada +1F1E8 1F1E8 ; fully-qualified # 🇨🇨 flag: Cocos (Keeling) Islands +1F1E8 1F1E9 ; fully-qualified # 🇨🇩 flag: Congo - Kinshasa +1F1E8 1F1EB ; fully-qualified # 🇨🇫 flag: Central African Republic +1F1E8 1F1EC ; fully-qualified # 🇨🇬 flag: Congo - Brazzaville +1F1E8 1F1ED ; fully-qualified # 🇨🇭 flag: Switzerland +1F1E8 1F1EE ; fully-qualified # 🇨🇮 flag: Côte d’Ivoire +1F1E8 1F1F0 ; fully-qualified # 🇨🇰 flag: Cook Islands +1F1E8 1F1F1 ; fully-qualified # 🇨🇱 flag: Chile +1F1E8 1F1F2 ; fully-qualified # 🇨🇲 flag: Cameroon +1F1E8 1F1F3 ; fully-qualified # 🇨🇳 flag: China +1F1E8 1F1F4 ; fully-qualified # 🇨🇴 flag: Colombia +1F1E8 1F1F5 ; fully-qualified # 🇨🇵 flag: Clipperton Island +1F1E8 1F1F7 ; fully-qualified # 🇨🇷 flag: Costa Rica +1F1E8 1F1FA ; fully-qualified # 🇨🇺 flag: Cuba +1F1E8 1F1FB ; fully-qualified # 🇨🇻 flag: Cape Verde +1F1E8 1F1FC ; fully-qualified # 🇨🇼 flag: Curaçao +1F1E8 1F1FD ; fully-qualified # 🇨🇽 flag: Christmas Island +1F1E8 1F1FE ; fully-qualified # 🇨🇾 flag: Cyprus +1F1E8 1F1FF ; fully-qualified # 🇨🇿 flag: Czechia +1F1E9 1F1EA ; fully-qualified # 🇩🇪 flag: Germany +1F1E9 1F1EC ; fully-qualified # 🇩🇬 flag: Diego Garcia +1F1E9 1F1EF ; fully-qualified # 🇩🇯 flag: Djibouti +1F1E9 1F1F0 ; fully-qualified # 🇩🇰 flag: Denmark +1F1E9 1F1F2 ; fully-qualified # 🇩🇲 flag: Dominica +1F1E9 1F1F4 ; fully-qualified # 🇩🇴 flag: Dominican Republic +1F1E9 1F1FF ; fully-qualified # 🇩🇿 flag: Algeria +1F1EA 1F1E6 ; fully-qualified # 🇪🇦 flag: Ceuta & Melilla +1F1EA 1F1E8 ; fully-qualified # 🇪🇨 flag: Ecuador +1F1EA 1F1EA ; fully-qualified # 🇪🇪 flag: Estonia +1F1EA 1F1EC ; fully-qualified # 🇪🇬 flag: Egypt +1F1EA 1F1ED ; fully-qualified # 🇪🇭 flag: Western Sahara +1F1EA 1F1F7 ; fully-qualified # 🇪🇷 flag: Eritrea +1F1EA 1F1F8 ; fully-qualified # 🇪🇸 flag: Spain +1F1EA 1F1F9 ; fully-qualified # 🇪🇹 flag: Ethiopia +1F1EA 1F1FA ; fully-qualified # 🇪🇺 flag: European Union +1F1EB 1F1EE ; fully-qualified # 🇫🇮 flag: Finland +1F1EB 1F1EF ; fully-qualified # 🇫🇯 flag: Fiji +1F1EB 1F1F0 ; fully-qualified # 🇫🇰 flag: Falkland Islands +1F1EB 1F1F2 ; fully-qualified # 🇫🇲 flag: Micronesia +1F1EB 1F1F4 ; fully-qualified # 🇫🇴 flag: Faroe Islands +1F1EB 1F1F7 ; fully-qualified # 🇫🇷 flag: France +1F1EC 1F1E6 ; fully-qualified # 🇬🇦 flag: Gabon +1F1EC 1F1E7 ; fully-qualified # 🇬🇧 flag: United Kingdom +1F1EC 1F1E9 ; fully-qualified # 🇬🇩 flag: Grenada +1F1EC 1F1EA ; fully-qualified # 🇬🇪 flag: Georgia +1F1EC 1F1EB ; fully-qualified # 🇬🇫 flag: French Guiana +1F1EC 1F1EC ; fully-qualified # 🇬🇬 flag: Guernsey +1F1EC 1F1ED ; fully-qualified # 🇬🇭 flag: Ghana +1F1EC 1F1EE ; fully-qualified # 🇬🇮 flag: Gibraltar +1F1EC 1F1F1 ; fully-qualified # 🇬🇱 flag: Greenland +1F1EC 1F1F2 ; fully-qualified # 🇬🇲 flag: Gambia +1F1EC 1F1F3 ; fully-qualified # 🇬🇳 flag: Guinea +1F1EC 1F1F5 ; fully-qualified # 🇬🇵 flag: Guadeloupe +1F1EC 1F1F6 ; fully-qualified # 🇬🇶 flag: Equatorial Guinea +1F1EC 1F1F7 ; fully-qualified # 🇬🇷 flag: Greece +1F1EC 1F1F8 ; fully-qualified # 🇬🇸 flag: South Georgia & South Sandwich Islands +1F1EC 1F1F9 ; fully-qualified # 🇬🇹 flag: Guatemala +1F1EC 1F1FA ; fully-qualified # 🇬🇺 flag: Guam +1F1EC 1F1FC ; fully-qualified # 🇬🇼 flag: Guinea-Bissau +1F1EC 1F1FE ; fully-qualified # 🇬🇾 flag: Guyana +1F1ED 1F1F0 ; fully-qualified # 🇭🇰 flag: Hong Kong SAR China +1F1ED 1F1F2 ; fully-qualified # 🇭🇲 flag: Heard & McDonald Islands +1F1ED 1F1F3 ; fully-qualified # 🇭🇳 flag: Honduras +1F1ED 1F1F7 ; fully-qualified # 🇭🇷 flag: Croatia +1F1ED 1F1F9 ; fully-qualified # 🇭🇹 flag: Haiti +1F1ED 1F1FA ; fully-qualified # 🇭🇺 flag: Hungary +1F1EE 1F1E8 ; fully-qualified # 🇮🇨 flag: Canary Islands +1F1EE 1F1E9 ; fully-qualified # 🇮🇩 flag: Indonesia +1F1EE 1F1EA ; fully-qualified # 🇮🇪 flag: Ireland +1F1EE 1F1F1 ; fully-qualified # 🇮🇱 flag: Israel +1F1EE 1F1F2 ; fully-qualified # 🇮🇲 flag: Isle of Man +1F1EE 1F1F3 ; fully-qualified # 🇮🇳 flag: India +1F1EE 1F1F4 ; fully-qualified # 🇮🇴 flag: British Indian Ocean Territory +1F1EE 1F1F6 ; fully-qualified # 🇮🇶 flag: Iraq +1F1EE 1F1F7 ; fully-qualified # 🇮🇷 flag: Iran +1F1EE 1F1F8 ; fully-qualified # 🇮🇸 flag: Iceland +1F1EE 1F1F9 ; fully-qualified # 🇮🇹 flag: Italy +1F1EF 1F1EA ; fully-qualified # 🇯🇪 flag: Jersey +1F1EF 1F1F2 ; fully-qualified # 🇯🇲 flag: Jamaica +1F1EF 1F1F4 ; fully-qualified # 🇯🇴 flag: Jordan +1F1EF 1F1F5 ; fully-qualified # 🇯🇵 flag: Japan +1F1F0 1F1EA ; fully-qualified # 🇰🇪 flag: Kenya +1F1F0 1F1EC ; fully-qualified # 🇰🇬 flag: Kyrgyzstan +1F1F0 1F1ED ; fully-qualified # 🇰🇭 flag: Cambodia +1F1F0 1F1EE ; fully-qualified # 🇰🇮 flag: Kiribati +1F1F0 1F1F2 ; fully-qualified # 🇰🇲 flag: Comoros +1F1F0 1F1F3 ; fully-qualified # 🇰🇳 flag: St. Kitts & Nevis +1F1F0 1F1F5 ; fully-qualified # 🇰🇵 flag: North Korea +1F1F0 1F1F7 ; fully-qualified # 🇰🇷 flag: South Korea +1F1F0 1F1FC ; fully-qualified # 🇰🇼 flag: Kuwait +1F1F0 1F1FE ; fully-qualified # 🇰🇾 flag: Cayman Islands +1F1F0 1F1FF ; fully-qualified # 🇰🇿 flag: Kazakhstan +1F1F1 1F1E6 ; fully-qualified # 🇱🇦 flag: Laos +1F1F1 1F1E7 ; fully-qualified # 🇱🇧 flag: Lebanon +1F1F1 1F1E8 ; fully-qualified # 🇱🇨 flag: St. Lucia +1F1F1 1F1EE ; fully-qualified # 🇱🇮 flag: Liechtenstein +1F1F1 1F1F0 ; fully-qualified # 🇱🇰 flag: Sri Lanka +1F1F1 1F1F7 ; fully-qualified # 🇱🇷 flag: Liberia +1F1F1 1F1F8 ; fully-qualified # 🇱🇸 flag: Lesotho +1F1F1 1F1F9 ; fully-qualified # 🇱🇹 flag: Lithuania +1F1F1 1F1FA ; fully-qualified # 🇱🇺 flag: Luxembourg +1F1F1 1F1FB ; fully-qualified # 🇱🇻 flag: Latvia +1F1F1 1F1FE ; fully-qualified # 🇱🇾 flag: Libya +1F1F2 1F1E6 ; fully-qualified # 🇲🇦 flag: Morocco +1F1F2 1F1E8 ; fully-qualified # 🇲🇨 flag: Monaco +1F1F2 1F1E9 ; fully-qualified # 🇲🇩 flag: Moldova +1F1F2 1F1EA ; fully-qualified # 🇲🇪 flag: Montenegro +1F1F2 1F1EB ; fully-qualified # 🇲🇫 flag: St. Martin +1F1F2 1F1EC ; fully-qualified # 🇲🇬 flag: Madagascar +1F1F2 1F1ED ; fully-qualified # 🇲🇭 flag: Marshall Islands +1F1F2 1F1F0 ; fully-qualified # 🇲🇰 flag: Macedonia +1F1F2 1F1F1 ; fully-qualified # 🇲🇱 flag: Mali +1F1F2 1F1F2 ; fully-qualified # 🇲🇲 flag: Myanmar (Burma) +1F1F2 1F1F3 ; fully-qualified # 🇲🇳 flag: Mongolia +1F1F2 1F1F4 ; fully-qualified # 🇲🇴 flag: Macao SAR China +1F1F2 1F1F5 ; fully-qualified # 🇲🇵 flag: Northern Mariana Islands +1F1F2 1F1F6 ; fully-qualified # 🇲🇶 flag: Martinique +1F1F2 1F1F7 ; fully-qualified # 🇲🇷 flag: Mauritania +1F1F2 1F1F8 ; fully-qualified # 🇲🇸 flag: Montserrat +1F1F2 1F1F9 ; fully-qualified # 🇲🇹 flag: Malta +1F1F2 1F1FA ; fully-qualified # 🇲🇺 flag: Mauritius +1F1F2 1F1FB ; fully-qualified # 🇲🇻 flag: Maldives +1F1F2 1F1FC ; fully-qualified # 🇲🇼 flag: Malawi +1F1F2 1F1FD ; fully-qualified # 🇲🇽 flag: Mexico +1F1F2 1F1FE ; fully-qualified # 🇲🇾 flag: Malaysia +1F1F2 1F1FF ; fully-qualified # 🇲🇿 flag: Mozambique +1F1F3 1F1E6 ; fully-qualified # 🇳🇦 flag: Namibia +1F1F3 1F1E8 ; fully-qualified # 🇳🇨 flag: New Caledonia +1F1F3 1F1EA ; fully-qualified # 🇳🇪 flag: Niger +1F1F3 1F1EB ; fully-qualified # 🇳🇫 flag: Norfolk Island +1F1F3 1F1EC ; fully-qualified # 🇳🇬 flag: Nigeria +1F1F3 1F1EE ; fully-qualified # 🇳🇮 flag: Nicaragua +1F1F3 1F1F1 ; fully-qualified # 🇳🇱 flag: Netherlands +1F1F3 1F1F4 ; fully-qualified # 🇳🇴 flag: Norway +1F1F3 1F1F5 ; fully-qualified # 🇳🇵 flag: Nepal +1F1F3 1F1F7 ; fully-qualified # 🇳🇷 flag: Nauru +1F1F3 1F1FA ; fully-qualified # 🇳🇺 flag: Niue +1F1F3 1F1FF ; fully-qualified # 🇳🇿 flag: New Zealand +1F1F4 1F1F2 ; fully-qualified # 🇴🇲 flag: Oman +1F1F5 1F1E6 ; fully-qualified # 🇵🇦 flag: Panama +1F1F5 1F1EA ; fully-qualified # 🇵🇪 flag: Peru +1F1F5 1F1EB ; fully-qualified # 🇵🇫 flag: French Polynesia +1F1F5 1F1EC ; fully-qualified # 🇵🇬 flag: Papua New Guinea +1F1F5 1F1ED ; fully-qualified # 🇵🇭 flag: Philippines +1F1F5 1F1F0 ; fully-qualified # 🇵🇰 flag: Pakistan +1F1F5 1F1F1 ; fully-qualified # 🇵🇱 flag: Poland +1F1F5 1F1F2 ; fully-qualified # 🇵🇲 flag: St. Pierre & Miquelon +1F1F5 1F1F3 ; fully-qualified # 🇵🇳 flag: Pitcairn Islands +1F1F5 1F1F7 ; fully-qualified # 🇵🇷 flag: Puerto Rico +1F1F5 1F1F8 ; fully-qualified # 🇵🇸 flag: Palestinian Territories +1F1F5 1F1F9 ; fully-qualified # 🇵🇹 flag: Portugal +1F1F5 1F1FC ; fully-qualified # 🇵🇼 flag: Palau +1F1F5 1F1FE ; fully-qualified # 🇵🇾 flag: Paraguay +1F1F6 1F1E6 ; fully-qualified # 🇶🇦 flag: Qatar +1F1F7 1F1EA ; fully-qualified # 🇷🇪 flag: Réunion +1F1F7 1F1F4 ; fully-qualified # 🇷🇴 flag: Romania +1F1F7 1F1F8 ; fully-qualified # 🇷🇸 flag: Serbia +1F1F7 1F1FA ; fully-qualified # 🇷🇺 flag: Russia +1F1F7 1F1FC ; fully-qualified # 🇷🇼 flag: Rwanda +1F1F8 1F1E6 ; fully-qualified # 🇸🇦 flag: Saudi Arabia +1F1F8 1F1E7 ; fully-qualified # 🇸🇧 flag: Solomon Islands +1F1F8 1F1E8 ; fully-qualified # 🇸🇨 flag: Seychelles +1F1F8 1F1E9 ; fully-qualified # 🇸🇩 flag: Sudan +1F1F8 1F1EA ; fully-qualified # 🇸🇪 flag: Sweden +1F1F8 1F1EC ; fully-qualified # 🇸🇬 flag: Singapore +1F1F8 1F1ED ; fully-qualified # 🇸🇭 flag: St. Helena +1F1F8 1F1EE ; fully-qualified # 🇸🇮 flag: Slovenia +1F1F8 1F1EF ; fully-qualified # 🇸🇯 flag: Svalbard & Jan Mayen +1F1F8 1F1F0 ; fully-qualified # 🇸🇰 flag: Slovakia +1F1F8 1F1F1 ; fully-qualified # 🇸🇱 flag: Sierra Leone +1F1F8 1F1F2 ; fully-qualified # 🇸🇲 flag: San Marino +1F1F8 1F1F3 ; fully-qualified # 🇸🇳 flag: Senegal +1F1F8 1F1F4 ; fully-qualified # 🇸🇴 flag: Somalia +1F1F8 1F1F7 ; fully-qualified # 🇸🇷 flag: Suriname +1F1F8 1F1F8 ; fully-qualified # 🇸🇸 flag: South Sudan +1F1F8 1F1F9 ; fully-qualified # 🇸🇹 flag: São Tomé & Príncipe +1F1F8 1F1FB ; fully-qualified # 🇸🇻 flag: El Salvador +1F1F8 1F1FD ; fully-qualified # 🇸🇽 flag: Sint Maarten +1F1F8 1F1FE ; fully-qualified # 🇸🇾 flag: Syria +1F1F8 1F1FF ; fully-qualified # 🇸🇿 flag: Eswatini +1F1F9 1F1E6 ; fully-qualified # 🇹🇦 flag: Tristan da Cunha +1F1F9 1F1E8 ; fully-qualified # 🇹🇨 flag: Turks & Caicos Islands +1F1F9 1F1E9 ; fully-qualified # 🇹🇩 flag: Chad +1F1F9 1F1EB ; fully-qualified # 🇹🇫 flag: French Southern Territories +1F1F9 1F1EC ; fully-qualified # 🇹🇬 flag: Togo +1F1F9 1F1ED ; fully-qualified # 🇹🇭 flag: Thailand +1F1F9 1F1EF ; fully-qualified # 🇹🇯 flag: Tajikistan +1F1F9 1F1F0 ; fully-qualified # 🇹🇰 flag: Tokelau +1F1F9 1F1F1 ; fully-qualified # 🇹🇱 flag: Timor-Leste +1F1F9 1F1F2 ; fully-qualified # 🇹🇲 flag: Turkmenistan +1F1F9 1F1F3 ; fully-qualified # 🇹🇳 flag: Tunisia +1F1F9 1F1F4 ; fully-qualified # 🇹🇴 flag: Tonga +1F1F9 1F1F7 ; fully-qualified # 🇹🇷 flag: Turkey +1F1F9 1F1F9 ; fully-qualified # 🇹🇹 flag: Trinidad & Tobago +1F1F9 1F1FB ; fully-qualified # 🇹🇻 flag: Tuvalu +1F1F9 1F1FC ; fully-qualified # 🇹🇼 flag: Taiwan +1F1F9 1F1FF ; fully-qualified # 🇹🇿 flag: Tanzania +1F1FA 1F1E6 ; fully-qualified # 🇺🇦 flag: Ukraine +1F1FA 1F1EC ; fully-qualified # 🇺🇬 flag: Uganda +1F1FA 1F1F2 ; fully-qualified # 🇺🇲 flag: U.S. Outlying Islands +1F1FA 1F1F3 ; fully-qualified # 🇺🇳 flag: United Nations +1F1FA 1F1F8 ; fully-qualified # 🇺🇸 flag: United States +1F1FA 1F1FE ; fully-qualified # 🇺🇾 flag: Uruguay +1F1FA 1F1FF ; fully-qualified # 🇺🇿 flag: Uzbekistan +1F1FB 1F1E6 ; fully-qualified # 🇻🇦 flag: Vatican City +1F1FB 1F1E8 ; fully-qualified # 🇻🇨 flag: St. Vincent & Grenadines +1F1FB 1F1EA ; fully-qualified # 🇻🇪 flag: Venezuela +1F1FB 1F1EC ; fully-qualified # 🇻🇬 flag: British Virgin Islands +1F1FB 1F1EE ; fully-qualified # 🇻🇮 flag: U.S. Virgin Islands +1F1FB 1F1F3 ; fully-qualified # 🇻🇳 flag: Vietnam +1F1FB 1F1FA ; fully-qualified # 🇻🇺 flag: Vanuatu +1F1FC 1F1EB ; fully-qualified # 🇼🇫 flag: Wallis & Futuna +1F1FC 1F1F8 ; fully-qualified # 🇼🇸 flag: Samoa +1F1FD 1F1F0 ; fully-qualified # 🇽🇰 flag: Kosovo +1F1FE 1F1EA ; fully-qualified # 🇾🇪 flag: Yemen +1F1FE 1F1F9 ; fully-qualified # 🇾🇹 flag: Mayotte +1F1FF 1F1E6 ; fully-qualified # 🇿🇦 flag: South Africa +1F1FF 1F1F2 ; fully-qualified # 🇿🇲 flag: Zambia +1F1FF 1F1FC ; fully-qualified # 🇿🇼 flag: Zimbabwe + +# subgroup: subdivision-flag +1F3F4 E0067 E0062 E0065 E006E E0067 E007F ; fully-qualified # 🏴󠁧󠁢󠁥󠁮󠁧󠁿 flag: England +1F3F4 E0067 E0062 E0073 E0063 E0074 E007F ; fully-qualified # 🏴󠁧󠁢󠁳󠁣󠁴󠁿 flag: Scotland +1F3F4 E0067 E0062 E0077 E006C E0073 E007F ; fully-qualified # 🏴󠁧󠁢󠁷󠁬󠁳󠁿 flag: Wales + +# Flags subtotal: 271 +# Flags subtotal: 271 w/o modifiers + +# Status Counts +# fully-qualified : 3010 +# component : 9 + +#EOF diff --git a/app/userland/app-stdlib/scripts/generate-css-js.js b/app/userland/app-stdlib/scripts/generate-css-js.js new file mode 100644 index 0000000000..b57fc811be --- /dev/null +++ b/app/userland/app-stdlib/scripts/generate-css-js.js @@ -0,0 +1,64 @@ +const fs = require('fs') +const path = require('path') + +const litElementPath = path.join(__dirname, '..', 'vendor', 'lit-element', 'lit-element.js') +console.log('Path:', litElementPath) +const cssdir = path.join(__dirname, '..', 'css') +handleFolder(cssdir) + +function handleFolder (dirpath) { + console.log('->', dirpath) + for (let name of fs.readdirSync(dirpath)) { + let itempath = path.join(dirpath, name) + let stat = fs.statSync(itempath) + if (stat.isDirectory()) { + handleFolder(itempath) + } else if (itempath.endsWith('.css')) { + handleCSSFile(itempath) + } + } +} + +// generates the css-js files +function handleCSSFile (cssPath) { + const cssJsPath = cssPathToJsPath(cssPath) + console.log('Generating', cssJsPath) + + // read the css + const css = fs.readFileSync(cssPath, 'utf8') + + // replace the css imports with js imports + const [newCss, imports] = extractAndReplaceImports(css) + + // write the css-js file + fs.writeFileSync(cssJsPath, `import {css} from '${path.relative(path.dirname(cssPath), litElementPath)}' +${imports} + +const cssStr = css\` +${newCss} +\` +export default cssStr +`) +} + +// converts a css path to a css-js path +// eg reset.css -> reset.css.js +function cssPathToJsPath (cssPath) { + return cssPath.slice(0, cssPath.length - '.css'.length) + '.css.js' +} + +// finds all css imports and converts them into css-js module imports +// eg @import "./reset.less" -> import resetcss from './reset.css.js' +function extractAndReplaceImports (css) { + var imports = [] + var newCss = css.replace(/^@import "([^"]*)";$/gm, (line, path) => { + const importObj = { + path: cssPathToJsPath(path), + varname: path.split('/').pop().replace(/\./g, '').replace(/-/g, '') + } + imports.push(importObj) + return `\${${importObj.varname}}` + }) + var importsStr = imports.map(i => `import ${i.varname} from '${i.path}'`).join('\n') + return [newCss, importsStr] +} \ No newline at end of file diff --git a/app/userland/app-stdlib/scripts/generate-emoji-list.js b/app/userland/app-stdlib/scripts/generate-emoji-list.js new file mode 100644 index 0000000000..dd8c2d53a8 --- /dev/null +++ b/app/userland/app-stdlib/scripts/generate-emoji-list.js @@ -0,0 +1,70 @@ +const fs = require('fs') + +const DISALLOWED = new Set([ + "🔫", + "🔪", + "🖕", + "🗡️" +]) + +var emojiDataStr = fs.readFileSync(require('path').join(__dirname, 'emoji-data.txt'), 'utf8') + +var groups = [] +for (let groupStr of emojiDataStr.split('# group: ').slice(1)) { + let name = (/(.*)\n/.exec(groupStr))[1] + let emojis = new Set() + + if (name === 'Component') { + continue // skip + } + + let re = /$([0-9A-F\.\s]+);/gim + let match + while (match = re.exec(groupStr)) { + let emoji = match[1].trim().split(' ').map(v => String.fromCodePoint(parseInt(v, 16))).join('') // parse out emoji + emoji = emoji.replace(/🏻|🏼|🏽|🏾|🏿/g, '') // strip skin tones + if (DISALLOWED.has(emoji)) continue // skip disallowed emojis + emojis.add(emoji) + } + + groups.push({name, emojis: Array.from(emojis)}) +} + +fs.writeFileSync(require('path').join(__dirname, 'emoji-list.js'), ` +export const SUGGESTED = [ + "❤", + "👀", + "🔥", + "🎉", + "✨", + "🆒", + '🙂', + '😂', + "😅", + '😢', + "😐", + "😮", + '😡', + "😤", + "🤭", + "🤔", + "🤨", + "🤯", + '👍', + "👎", + "👆", + "👏", + "🙌", + "🙏", + "👋", + "💪", + "💅", + "✊", + "👌", + "🤘", +] + +export const GROUPS = ${JSON.stringify(groups, null, 2)} + +export const FULL_LIST = GROUPS.map(({emojis}) => emojis).reduce((acc, v) => acc.concat(v), []) +`) \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/bytes/.editorconfig b/app/userland/app-stdlib/vendor/bytes/.editorconfig new file mode 100755 index 0000000000..cdb36c1b46 --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/.editorconfig @@ -0,0 +1,11 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[{*.js,*.json,*.yml}] +indent_size = 2 +indent_style = space diff --git a/app/userland/app-stdlib/vendor/bytes/.eslintignore b/app/userland/app-stdlib/vendor/bytes/.eslintignore new file mode 100755 index 0000000000..76b1021d09 --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/.eslintignore @@ -0,0 +1,3 @@ +.nyc_output +coverage +node_modules diff --git a/app/userland/app-stdlib/vendor/bytes/.eslintrc.yml b/app/userland/app-stdlib/vendor/bytes/.eslintrc.yml new file mode 100755 index 0000000000..c1475b20ba --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/.eslintrc.yml @@ -0,0 +1,9 @@ +root: true +extends: eslint:recommended +env: + node: true +rules: + eol-last: error + indent: ["error", 2, { "SwitchCase": 1 }] + no-mixed-spaces-and-tabs: error + no-trailing-spaces: error diff --git a/app/userland/app-stdlib/vendor/bytes/.gitignore b/app/userland/app-stdlib/vendor/bytes/.gitignore new file mode 100755 index 0000000000..f15b98e249 --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/.gitignore @@ -0,0 +1,5 @@ +.nyc_output/ +coverage/ +node_modules/ +npm-debug.log +package-lock.json diff --git a/app/userland/app-stdlib/vendor/bytes/.travis.yml b/app/userland/app-stdlib/vendor/bytes/.travis.yml new file mode 100755 index 0000000000..5c5e0edd88 --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/.travis.yml @@ -0,0 +1,77 @@ +language: node_js +node_js: + - "0.8" + - "0.10" + - "0.12" + - "1.8" + - "2.5" + - "3.3" + - "4.9" + - "5.12" + - "6.16" + - "7.10" + - "8.15" + - "9.11" + - "10.15" + - "11.7" +sudo: false +cache: + directories: + - node_modules +before_install: + # Configure npm + - | + # Skip updating shrinkwrap / lock + npm config set shrinkwrap false + # Setup Node.js version-specific dependencies + - | + # mocha for testing + # - use 2.x for Node.js < 0.10 + # - use 3.x for Node.js < 6 + if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -eq 0 && "$(cut -d. -f2 <<< "$TRAVIS_NODE_VERSION")" -lt 10 ]]; then + npm install --save-dev mocha@2.5.3 + elif [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then + npm install --save-dev mocha@3.5.3 + fi + - | + # nyc for coverage + # - remove on Node.js < 6 + if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then + npm rm --save-dev nyc + fi + - | + # eslint for linting + # - remove on Node.js < 6 + if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then + node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ + grep -E '^eslint(-|$)' | \ + xargs npm rm --save-dev + fi + # Update Node.js modules + - | + # Prune & rebuild node_modules + if [[ -d node_modules ]]; then + npm prune + npm rebuild + fi + +script: + - | + # Run test script, depending on nyc install + if [[ -n "$(npm -ps ls nyc)" ]]; then + npm run-script test-ci + else + npm test + fi + - | + # Run linting, depending on eslint install + if [[ -n "$(npm -ps ls eslint)" ]]; then + npm run-script lint + fi +after_script: + - | + # Upload coverage to coveralls, if exists + if [[ -d .nyc_output ]]; then + npm install --save-dev coveralls@2 + nyc report --reporter=text-lcov | coveralls + fi diff --git a/app/userland/app-stdlib/vendor/bytes/History.md b/app/userland/app-stdlib/vendor/bytes/History.md new file mode 100755 index 0000000000..cf6a5bb9cf --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/History.md @@ -0,0 +1,87 @@ +3.1.0 / 2019-01-22 +================== + + * Add petabyte (`pb`) support + +3.0.0 / 2017-08-31 +================== + + * Change "kB" to "KB" in format output + * Remove support for Node.js 0.6 + * Remove support for ComponentJS + +2.5.0 / 2017-03-24 +================== + + * Add option "unit" + +2.4.0 / 2016-06-01 +================== + + * Add option "unitSeparator" + +2.3.0 / 2016-02-15 +================== + + * Drop partial bytes on all parsed units + * Fix non-finite numbers to `.format` to return `null` + * Fix parsing byte string that looks like hex + * perf: hoist regular expressions + +2.2.0 / 2015-11-13 +================== + + * add option "decimalPlaces" + * add option "fixedDecimals" + +2.1.0 / 2015-05-21 +================== + + * add `.format` export + * add `.parse` export + +2.0.2 / 2015-05-20 +================== + + * remove map recreation + * remove unnecessary object construction + +2.0.1 / 2015-05-07 +================== + + * fix browserify require + * remove node.extend dependency + +2.0.0 / 2015-04-12 +================== + + * add option "case" + * add option "thousandsSeparator" + * return "null" on invalid parse input + * support proper round-trip: bytes(bytes(num)) === num + * units no longer case sensitive when parsing + +1.0.0 / 2014-05-05 +================== + + * add negative support. fixes #6 + +0.3.0 / 2014-03-19 +================== + + * added terabyte support + +0.2.1 / 2013-04-01 +================== + + * add .component + +0.2.0 / 2012-10-28 +================== + + * bytes(200).should.eql('200b') + +0.1.0 / 2012-07-04 +================== + + * add bytes to string conversion [yields] diff --git a/app/userland/app-stdlib/vendor/bytes/LICENSE b/app/userland/app-stdlib/vendor/bytes/LICENSE new file mode 100755 index 0000000000..63e95a9633 --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/app/userland/app-stdlib/vendor/bytes/Readme.md b/app/userland/app-stdlib/vendor/bytes/Readme.md new file mode 100755 index 0000000000..6ad1ec6e2a --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/Readme.md @@ -0,0 +1,126 @@ +# Bytes utility + +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![Build Status][travis-image]][travis-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Utility to parse a string bytes (ex: `1TB`) to bytes (`1099511627776`) and vice-versa. + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```bash +$ npm install bytes +``` + +## Usage + +```js +var bytes = require('bytes'); +``` + +#### bytes.format(number value, [options]): string|null + +Format the given value in bytes into a string. If the value is negative, it is kept as such. If it is a float, it is + rounded. + +**Arguments** + +| Name | Type | Description | +|---------|----------|--------------------| +| value | `number` | Value in bytes | +| options | `Object` | Conversion options | + +**Options** + +| Property | Type | Description | +|-------------------|--------|-----------------------------------------------------------------------------------------| +| decimalPlaces | `number`|`null` | Maximum number of decimal places to include in output. Default value to `2`. | +| fixedDecimals | `boolean`|`null` | Whether to always display the maximum number of decimal places. Default value to `false` | +| thousandsSeparator | `string`|`null` | Example of values: `' '`, `','` and `.`... Default value to `''`. | +| unit | `string`|`null` | The unit in which the result will be returned (B/KB/MB/GB/TB). Default value to `''` (which means auto detect). | +| unitSeparator | `string`|`null` | Separator to use between number and unit. Default value to `''`. | + +**Returns** + +| Name | Type | Description | +|---------|------------------|-------------------------------------------------| +| results | `string`|`null` | Return null upon error. String value otherwise. | + +**Example** + +```js +bytes(1024); +// output: '1KB' + +bytes(1000); +// output: '1000B' + +bytes(1000, {thousandsSeparator: ' '}); +// output: '1 000B' + +bytes(1024 * 1.7, {decimalPlaces: 0}); +// output: '2KB' + +bytes(1024, {unitSeparator: ' '}); +// output: '1 KB' + +``` + +#### bytes.parse(string|number value): number|null + +Parse the string value into an integer in bytes. If no unit is given, or `value` +is a number, it is assumed the value is in bytes. + +Supported units and abbreviations are as follows and are case-insensitive: + + * `b` for bytes + * `kb` for kilobytes + * `mb` for megabytes + * `gb` for gigabytes + * `tb` for terabytes + * `pb` for petabytes + +The units are in powers of two, not ten. This means 1kb = 1024b according to this parser. + +**Arguments** + +| Name | Type | Description | +|---------------|--------|--------------------| +| value | `string`|`number` | String to parse, or number in bytes. | + +**Returns** + +| Name | Type | Description | +|---------|-------------|-------------------------| +| results | `number`|`null` | Return null upon error. Value in bytes otherwise. | + +**Example** + +```js +bytes('1KB'); +// output: 1024 + +bytes('1024'); +// output: 1024 + +bytes(1024); +// output: 1KB +``` + +## License + +[MIT](LICENSE) + +[coveralls-image]: https://badgen.net/coveralls/c/github/visionmedia/bytes.js/master +[coveralls-url]: https://coveralls.io/r/visionmedia/bytes.js?branch=master +[downloads-image]: https://badgen.net/npm/dm/bytes +[downloads-url]: https://npmjs.org/package/bytes +[npm-image]: https://badgen.net/npm/node/bytes +[npm-url]: https://npmjs.org/package/bytes +[travis-image]: https://badgen.net/travis/visionmedia/bytes.js/master +[travis-url]: https://travis-ci.org/visionmedia/bytes.js diff --git a/app/userland/app-stdlib/vendor/bytes/index.js b/app/userland/app-stdlib/vendor/bytes/index.js new file mode 100755 index 0000000000..0e83a8fb89 --- /dev/null +++ b/app/userland/app-stdlib/vendor/bytes/index.js @@ -0,0 +1,153 @@ +/*! + * bytes + * Copyright(c) 2012-2014 TJ Holowaychuk + * Copyright(c) 2015 Jed Watson + * MIT Licensed + */ + +'use strict'; + +/** + * Module variables. + * @private + */ + +var formatThousandsRegExp = /\B(?=(\d{3})+(?!\d))/g; + +var formatDecimalsRegExp = /(?:\.0*|(\.[^0]+)0+)$/; + +var map = { + b: 1, + kb: 1 << 10, + mb: 1 << 20, + gb: 1 << 30, + tb: Math.pow(1024, 4), + pb: Math.pow(1024, 5), +}; + +var parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i; + +/** + * Convert the given value in bytes into a string or parse to string to an integer in bytes. + * + * @param {string|number} value + * @param {{ + * case: [string], + * decimalPlaces: [number] + * fixedDecimals: [boolean] + * thousandsSeparator: [string] + * unitSeparator: [string] + * }} [options] bytes options. + * + * @returns {string|number|null} + */ + +export default function bytes(value, options) { + if (typeof value === 'string') { + return parse(value); + } + + if (typeof value === 'number') { + return format(value, options); + } + + return null; +} + +/** + * Format the given value in bytes into a string. + * + * If the value is negative, it is kept as such. If it is a float, + * it is rounded. + * + * @param {number} value + * @param {object} [options] + * @param {number} [options.decimalPlaces=2] + * @param {number} [options.fixedDecimals=false] + * @param {string} [options.thousandsSeparator=] + * @param {string} [options.unit=] + * @param {string} [options.unitSeparator=] + * + * @returns {string|null} + * @public + */ + +export function format(value, options) { + if (!Number.isFinite(value)) { + return null; + } + + var mag = Math.abs(value); + var thousandsSeparator = (options && options.thousandsSeparator) || ''; + var unitSeparator = (options && options.unitSeparator) || ''; + var decimalPlaces = (options && options.decimalPlaces !== undefined) ? options.decimalPlaces : 2; + var fixedDecimals = Boolean(options && options.fixedDecimals); + var unit = (options && options.unit) || ''; + + if (!unit || !map[unit.toLowerCase()]) { + if (mag >= map.pb) { + unit = 'PB'; + } else if (mag >= map.tb) { + unit = 'TB'; + } else if (mag >= map.gb) { + unit = 'GB'; + } else if (mag >= map.mb) { + unit = 'MB'; + } else if (mag >= map.kb) { + unit = 'KB'; + } else { + unit = 'B'; + } + } + + var val = value / map[unit.toLowerCase()]; + var str = val.toFixed(decimalPlaces); + + if (!fixedDecimals) { + str = str.replace(formatDecimalsRegExp, '$1'); + } + + if (thousandsSeparator) { + str = str.replace(formatThousandsRegExp, thousandsSeparator); + } + + return str + unitSeparator + unit; +} + +/** + * Parse the string value into an integer in bytes. + * + * If no unit is given, it is assumed the value is in bytes. + * + * @param {number|string} val + * + * @returns {number|null} + * @public + */ + +export function parse(val) { + if (typeof val === 'number' && !isNaN(val)) { + return val; + } + + if (typeof val !== 'string') { + return null; + } + + // Test if the string passed is valid + var results = parseRegExp.exec(val); + var floatValue; + var unit = 'b'; + + if (!results) { + // Nothing could be extracted from the given string + floatValue = parseInt(val, 10); + unit = 'b' + } else { + // Retrieve the value and the unit + floatValue = parseFloat(results[1]); + unit = results[4].toLowerCase(); + } + + return Math.floor(map[unit] * floatValue); +} diff --git a/app/userland/app-stdlib/vendor/emoji-skin-tone/README.md b/app/userland/app-stdlib/vendor/emoji-skin-tone/README.md new file mode 100644 index 0000000000..bfe1bb2549 --- /dev/null +++ b/app/userland/app-stdlib/vendor/emoji-skin-tone/README.md @@ -0,0 +1,65 @@ +# skin-tone [![Build Status](https://travis-ci.org/sindresorhus/skin-tone.svg?branch=master)](https://travis-ci.org/sindresorhus/skin-tone) + +> Change the skin tone of an emoji 👌👌🏻👌🏼👌🏽👌🏾👌🏿 + +The [Fitzpatrick scale](https://en.wikipedia.org/wiki/Fitzpatrick_scale#Unicode) is used to specify skin tones for emoji characters which represent humans. + + +## Install + +``` +$ npm install --save skin-tone +``` + + +## Usage + +```js +const skinTone = require('skin-tone'); + +skinTone('👍', skinTone.BROWN); +//=> '👍🏾' + +// or by using the constant value directly +skinTone('👍', 4); +//=> '👍🏾 + +skinTone('👍', skinTone.WHITE); +//=> '👍🏻' + +// can also remove skin tone +skinTone('👍🏾', skinTone.NONE); +//=> '👍' + +// just passes it through when not supported +skinTone('🦄', skinTone.DARK_BROWN); +//=> '🦄' +``` + + +## API + +### skinTone(emoji, type) + +#### emoji + +Type: `string` + +Emoji to modify. + +#### type + +Type: `number`
+Values: + +- `skinTone.NONE` | `0`: *(Removes skin tone)* +- `skinTone.WHITE` | `1`: 🏻 *(Fitzpatrick Type-1–2)* +- `skinTone.CREAM_WHITE` | `2`: 🏼 *(Fitzpatrick Type-3)* +- `skinTone.LIGHT_BROWN` | `3`: 🏽 *(Fitzpatrick Type-4)* +- `skinTone.BROWN` | `4`: 🏾 *(Fitzpatrick Type-5)* +- `skinTone.DARK_BROWN` | `5`: 🏿 *(Fitzpatrick Type-6)* + + +## License + +MIT © [Sindre Sorhus](https://sindresorhus.com) diff --git a/app/userland/app-stdlib/vendor/emoji-skin-tone/index.js b/app/userland/app-stdlib/vendor/emoji-skin-tone/index.js new file mode 100644 index 0000000000..97373c0e50 --- /dev/null +++ b/app/userland/app-stdlib/vendor/emoji-skin-tone/index.js @@ -0,0 +1,162 @@ +/* +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + */ + +const emojiModifierBase = new Set([ + 0x261D, + 0x26F9, + 0x270A, + 0x270B, + 0x270C, + 0x270D, + 0x1F385, + 0x1F3C3, + 0x1F3C4, + 0x1F3CA, + 0x1F3CB, + 0x1F442, + 0x1F443, + 0x1F446, + 0x1F447, + 0x1F448, + 0x1F449, + 0x1F44A, + 0x1F44B, + 0x1F44C, + 0x1F44D, + 0x1F44E, + 0x1F44F, + 0x1F450, + 0x1F466, + 0x1F467, + // 0x1F468, SUPPORT (prf) + // 0x1F469, SUPPORT (prf) + 0x1F46E, + 0x1F470, + 0x1F471, + 0x1F472, + 0x1F473, + 0x1F474, + 0x1F475, + 0x1F476, + 0x1F477, + 0x1F478, + 0x1F47C, + 0x1F481, + 0x1F482, + 0x1F483, + 0x1F485, + 0x1F486, + 0x1F487, + 0x1F4AA, + 0x1F575, + 0x1F57A, + 0x1F590, + 0x1F595, + 0x1F596, + 0x1F645, + 0x1F646, + 0x1F647, + 0x1F64B, + 0x1F64C, + 0x1F64D, + 0x1F64E, + 0x1F64F, + 0x1F6A3, + 0x1F6B4, + 0x1F6B5, + 0x1F6B6, + 0x1F6C0, + 0x1F918, + 0x1F919, + 0x1F91A, + 0x1F91B, + 0x1F91C, + // 0x1F91D, SUPPORT (prf) + 0x1F91E, + 0x1F926, + 0x1F930, + 0x1F933, + 0x1F934, + 0x1F935, + 0x1F936, + 0x1F937, + 0x1F938, + 0x1F939, + // 0x1F93C, SUPPORT (prf) + 0x1F93D, + 0x1F93E +]); + + +const skinTones = [ + { + name: 'NONE', + color: '' + }, + { + name: 'WHITE', + color: '🏻' + }, + { + name: 'CREAM_WHITE', + color: '🏼' + }, + { + name: 'LIGHT_BROWN', + color: '🏽' + }, + { + name: 'BROWN', + color: '🏾' + }, + { + name: 'DARK_BROWN', + color: '🏿' + } +]; + +export const NONE = 0 +export const WHITE = 1 +export const CREAM_WHITE = 2 +export const LIGHT_BROWN = 3 +export const BROWN = 4 +export const DARK_BROWN = 5 + +export function set (emoji, type) { + if (type > 5 || type < 0) { + throw new TypeError(`Expected \`type\` to be a number between 0 and 5, got ${type}`); + } + + // TODO: Use this instead when targeting Node.js 6 + // emoji = emoji.replace(/[\u{1f3fb}-\u{1f3ff}]/u, ''); + skinTones.forEach(x => { + emoji = emoji.replace(x.color, ''); + }); + + if (emojiModifierBase.has(emoji.codePointAt(0)) && type !== 0) { + emoji += skinTones[type].color; + } + + return emoji; +} diff --git a/app/userland/app-stdlib/vendor/lit-element-router/README.md b/app/userland/app-stdlib/vendor/lit-element-router/README.md new file mode 100644 index 0000000000..a102734fb7 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element-router/README.md @@ -0,0 +1,207 @@ +# LitElement Router +A simple and lightweight LitElement Router. + +[![Coverage Status](https://coveralls.io/repos/github/hamedasemi/lit-element-router/badge.svg?branch=mainline)](https://coveralls.io/github/hamedasemi/lit-element-router?branch=mainline) +[![npm version](https://badge.fury.io/js/lit-element-router.svg)](https://badge.fury.io/js/lit-element-router) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/lit-element-router) +[![Known Vulnerabilities](https://snyk.io/test/github/hamedasemi/lit-element-router/badge.svg?targetFile=package.json)](https://snyk.io/test/github/hamedasemi/lit-element-router?targetFile=package.json) +[![CircleCI](https://circleci.com/gh/hamedasemi/lit-element-router/tree/mainline.svg?style=svg)](https://circleci.com/gh/hamedasemi/lit-element-router/tree/mainline) + + +## Installation + +```sh +npm install lit-element-router --save +``` + +## Usage + +### Working example +You can find a working project on StackBlitz https://stackblitz.com/edit/lit-element-router + +### Minimal +```js +import { LitElement, html } from 'lit-element'; +import { routerMixin } from 'lit-element-router'; + +class MyApp extends routerMixin(LitElement) { + + static get routes() { + return [{ + name: 'home', + pattern: '', + data: { title: 'Home' } + }, { + name: 'info', + pattern: 'info' + }, { + name: 'user', + pattern: 'user/:id' + }, { + name: 'not-found', + pattern: '*' + }]; + } + + onRoute(route, params, query, data) { + console.log(route, params, query, data) + } +} + +customElements.define('my-app', MyApp); +``` + + + +# Complete Example Using JavaScript Mixins in Details + +## Dont like mixins check out other examples +Don't want to use mixins interface you cane use a simple version in this tutorial: https://github.com/hamedasemi/lit-element-router/blob/mainline/README_NOT_MIXIN.md + +## Make any arbitary components or elements to a router using router mixins method +```javascript +import { LitElement, html } from 'lit-element'; +import { routerMixin } from 'lit-element-router'; + +class MyApp extends routerMixin(LitElement) { + +} + +customElements.define('my-app', MyApp); +``` + +## Register routes and the onRoute function +```javascript +import { LitElement, html } from 'lit-element'; +import { routerMixin } from 'lit-element-router'; + +class MyApp extends routerMixin(LitElement) { + static get routes() { + return [{ + name: 'home', + pattern: '' + }, { + name: 'info', + pattern: 'info' + }, { + name: 'user', + pattern: 'user/:id' + }, { + name: 'not-found', + pattern: '*' + }]; + } + + onRoute(route, params, query, data) { + this.route = route; + this.params = params; + } +} + +customElements.define('my-app', MyApp); +``` + + +## Make any arbitary components or elements to a router outlet using router outlet mixins method +```javascript +import { LitElement, html } from 'lit-element'; +import { routerOutletMixin } from 'lit-element-router'; + +export class AnyArbitaryLitElement extends routerOutletMixin(LitElement) { + +} + +customElements.define('any-arbitary-lit-element', AnyArbitaryLitElement); +``` + +## Put the components under router outlet +```javascript +import { LitElement, html } from 'lit-element'; +import { routerMixin } from 'lit-element-router'; + +class MyApp extends routerMixin(LitElement) { + static get routes() { + return [{ + name: 'home', + pattern: '' + }, { + name: 'info', + pattern: 'info' + }, { + name: 'user', + pattern: 'user/:id' + }, { + name: 'not-found', + pattern: '*' + }]; + } + + onRoute(route, params, query, data) { + this.route = route; + this.params = params; + } + + render() { + return html` + +
Home any-arbitary-lit-element
+
mY Info any-arbitary-lit-element
+
User ${this.params.id} any-arbitary-lit-element
+
Not Authorized any-arbitary-lit-element
+
Not Found any-arbitary-lit-element
+
+ `; +} + +customElements.define('my-app', MyApp); +``` + + +## Make any arbitary components or elements to a router link using router link mixins method +```javascript +import { LitElement, html } from 'lit-element'; +import { routerLinkMixin } from 'lit-element-router'; + +export class AnArbitaryLitElement extends routerLinkMixin(LitElement) { + +} + +customElements.define('an-arbitary-lit-element', AnArbitaryLitElement); +``` + +## Navigate using the router navigate method +```javascript +import { LitElement, html } from 'lit-element'; +import { routerLinkMixin } from 'lit-element-router'; + +export class AnArbitaryLitElement extends routerLinkMixin(LitElement) { + constructor() { + super() + this.href = '' + } + static get properties() { + return { + href: { type: String } + } + } + render() { + return html` + + ` + } + linkClick(event) { + event.preventDefault(); + this.navigate(this.href); + } +} + +customElements.define('an-arbitary-lit-element', AnArbitaryLitElement); +``` + + +## Browsers support + +| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [iOS Safari](http://godban.github.io/browsers-support-badges/)
iOS Safari | [Samsung](http://godban.github.io/browsers-support-badges/)
Samsung | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | +| --------- | --------- | --------- | --------- | --------- | --------- | --------- | +| IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions| last 2 versions| last 2 versions + diff --git a/app/userland/app-stdlib/vendor/lit-element-router/index.js b/app/userland/app-stdlib/vendor/lit-element-router/index.js new file mode 100644 index 0000000000..c6a31f1ac9 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element-router/index.js @@ -0,0 +1,108 @@ +import { parseParams, parseQuery, testRoute } from './utility/router-utility.js'; + +export let routerMixin = (superclass) => class extends superclass { + static get properties() { + return { + route: { type: String, reflect: true, attribute: 'route' }, + canceled: { type: Boolean } + } + } + + firstUpdated() { + + this.router(this.constructor.routes, (...args) => this.onRoute(...args)); + window.addEventListener('route', () => { + this.router(this.constructor.routes, (...args) => this.onRoute(...args)); + }) + + window.onpopstate = () => { + window.dispatchEvent(new CustomEvent('route')); + } + if (super.firstUpdated) super.firstUpdated(); + } + + router(routes, callback) { + this.canceled = true; + + const uri = decodeURI(window.location.pathname); + const querystring = decodeURI(window.location.search); + + let notFoundRoute = routes.filter(route => route.pattern === '*')[0]; + + routes = routes.filter(route => route.pattern !== '*' && testRoute(uri, route.pattern)); + + if (routes.length) { + let route = routes[0]; + route.params = parseParams(route.pattern, uri); + route.query = parseQuery(querystring); + + if (route.guard && typeof route.guard === 'function') { + + this.canceled = false + Promise.resolve(route.guard()) + .then((allowed) => { + if (!this.canceled) { + if (allowed) { + route.callback && route.callback(route.name, route.params, route.query, route.data) + callback(route.name, route.params, route.query, route.data); + } else { + route.callback && route.callback('not-authorized', route.params, route.query, route.data) + callback('not-authorized', {}, {}, {}); + } + } + }) + } else { + route.callback && route.callback(route.name, route.params, route.query, route.data) + callback(route.name, route.params, route.query, route.data); + } + } else if (notFoundRoute) { + notFoundRoute.callback && notFoundRoute.callback(notFoundRoute.name, {}, {}, {}) + callback(notFoundRoute.name, {}, {}, {}); + } else { + callback('not-found', {}, {}, {}); + } + + if (super.router) super.router(); + } +}; + +export let routerLinkMixin = (superclass) => class extends superclass { + + navigate(href) { + window.history.pushState({}, null, href + window.location.search); + window.dispatchEvent(new CustomEvent('route')); + + if (super.navigate) super.navigate(); + } +}; + +export let routerOutletMixin = (superclass) => class extends superclass { + + static get properties() { + return { + currentRoute: { type: String, reflect: true, attribute: 'current-route' } + } + } + + updated(updatedProperties) { + updatedProperties.has('currentRoute') && this.routerOutlet(); + if (super.updated) super.updated(); + } + + firstUpdated() { + this.routerOutlet(); + } + + routerOutlet() { + Array.from(this.shadowRoot.querySelectorAll(`[route]`)).map((selected) => { + this.appendChild(selected); + }); + if (this.currentRoute) { + Array.from(this.querySelectorAll(`[route~=${this.currentRoute}]`)).map((selected) => { + this.shadowRoot.appendChild(selected) + }); + } + + if (super.routerOutlet) super.routerOutlet(); + } +}; \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element-router/package.json b/app/userland/app-stdlib/vendor/lit-element-router/package.json new file mode 100644 index 0000000000..868c06cdde --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element-router/package.json @@ -0,0 +1,50 @@ +{ + "name": "lit-element-router", + "version": "1.2.3", + "main": "lit-element-router.js", + "scripts": { + "demo": "polymer serve", + "sync": "browser-sync start --proxy localhost:8081 --watch --files ./**/*.* --ignore node_modules --logLevel debug", + "test": "nyc mocha --require @babel/register './{,!(node_modules)/**/}*.test.js' --exit", + "report": "npm test && nyc report --reporter=text-lcov | COVERALLS_REPO_TOKEN=$COVERALLS_TOKEN coveralls", + "ver": "npm version prerelease --preid=beta", + "risk": "snyk monitor" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hamedasemi/lit-element-router.git" + }, + "keywords": [ + "lit", + "element", + "router", + "lit-element", + "lit-element-router", + "regexp" + ], + "author": "hamedabolghasemi", + "license": "ISC", + "bugs": { + "url": "https://github.com/hamedasemi/lit-element-router/issues" + }, + "homepage": "https://github.com/hamedasemi/lit-element-router#readme", + "devDependencies": { + "@babel/core": "^7.2.2", + "@babel/preset-env": "^7.3.1", + "@babel/register": "^7.0.0", + "@webcomponents/webcomponentsjs": "^2.2.7", + "asserts": "^4.0.2", + "chai": "^4.2.0", + "coveralls": "^3.0.2", + "lit-element": "^2.0.1", + "mocha": "^5.2.0", + "nyc": "^13.2.0", + "snyk": "^1.126.0" + }, + "dependencies": { + "lit-element": "^2.0.1" + }, + "nyc": { + "temp-dir": "./node_modules/.cache/alternative-tmp" + } +} diff --git a/app/userland/app-stdlib/vendor/lit-element-router/utility/router-utility.js b/app/userland/app-stdlib/vendor/lit-element-router/utility/router-utility.js new file mode 100644 index 0000000000..74d6334d43 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element-router/utility/router-utility.js @@ -0,0 +1,56 @@ +/** + * + * @param {String} str - The uri that has extra slashes + */ +export function stripExtraTrailingSlash(str) { + while (str.length !== 1 && str.substr(-1) === '/') { + str = str.substr(0, str.length - 1); + } + return str; +} + +/** +* +* @param {String} querystring - The author of the book. +*/ +export function parseQuery(querystring) { + return querystring ? JSON.parse('{"' + querystring.substring(1).replace(/&/g, '","').replace(/=/g, '":"') + '"}') : {} +} + +/** +* Desc +* @param {String} pattern - The pattern +* @param {String} uri - The current uri +* @return {Object} - The uri params object +*/ +export function parseParams(pattern, uri) { + let params = {} + + const patternArray = pattern.split('/').filter((path) => { return path != '' }) + const uriArray = uri.split('/').filter((path) => { return path != '' }) + + patternArray.map((pattern, i) => { + if (/^:/.test(pattern)) { + params[pattern.substring(1)] = uriArray[i] + } + }) + return params +} + +/** +* À-ÖØ-öø-ÿ +* @param {*} pattern +*/ +export function patternToRegExp(pattern) { + if (pattern) { + return new RegExp(pattern.replace(/:[^\s/]+/g, '([\\w\u00C0-\u00D6\u00D8-\u00f6\u00f8-\u00ff-]+)') + '(|/)$'); + } else { + return new RegExp('(^$|^/$)'); + } +} + +export function testRoute(uri, pattern) { + if (patternToRegExp(pattern).test(uri)) { + return true; + } +} \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/CHANGELOG.md b/app/userland/app-stdlib/vendor/lit-element/CHANGELOG.md new file mode 100755 index 0000000000..5ec5020707 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/CHANGELOG.md @@ -0,0 +1,150 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + + + + + + +## [2.0.1] - 2019-02-05 +### Fixed +* Use `lit-html` 1.0 ([#543](https://github.com/Polymer/lit-element/pull/543)). + +## [2.0.0] - 2019-02-05 +### Added +* Add `toString()` function to `CSSResult` ([#508](https://github.com/Polymer/lit-element/pull/508)) +* Add a global version to `window` ([#536](https://github.com/Polymer/lit-element/pull/536)) + +### Changed +* [Breaking] Renamed `unsafeCss` to `unsafeCSS` for consistency with lit-html's `unsafeHTML` ([#524](https://github.com/Polymer/lit-element/pull/524)) +* Remove all uses of `any` outside of tests ([#457](https://github.com/Polymer/lit-element/pull/457)) + +### Fixed +* A bunch of docs fixes ([#464](https://github.com/Polymer/lit-element/pull/464)), ([#458](https://github.com/Polymer/lit-element/pull/458)), ([#493](https://github.com/Polymer/lit-element/pull/493)), ([#504](https://github.com/Polymer/lit-element/pull/504)), ([#505](https://github.com/Polymer/lit-element/pull/505)), ([#501](https://github.com/Polymer/lit-element/pull/501)), ([#494](https://github.com/Polymer/lit-element/pull/494)), ([#491](https://github.com/Polymer/lit-element/pull/491)), ([#509](https://github.com/Polymer/lit-element/pull/509)), ([#513](https://github.com/Polymer/lit-element/pull/513)), ([#515](https://github.com/Polymer/lit-element/pull/515)), ([#512](https://github.com/Polymer/lit-element/pull/512)), ([#503](https://github.com/Polymer/lit-element/pull/503)), ([#460](https://github.com/Polymer/lit-element/pull/460)), ([#413](https://github.com/Polymer/lit-element/pull/413)), ([#426](https://github.com/Polymer/lit-element/pull/426)), ([#516](https://github.com/Polymer/lit-element/pull/516)), ([#537](https://github.com/Polymer/lit-element/pull/537)), ([#535](https://github.com/Polymer/lit-element/pull/535)), ([#539](https://github.com/Polymer/lit-element/pull/539)), ([#540](https://github.com/Polymer/lit-element/pull/540)) +* Build on checkout ([#423](https://github.com/Polymer/lit-element/pull/423)) + +### Fixed +* Adds a check to ensure `CSSStyleSheet` is constructable ([#527](https://github.com/Polymer/lit-element/pull/527)). + +## [2.0.0-rc.5] - 2019-01-24 +### Fixed +* Fixed a bug causing duplicate styles when an array was returned from `static get styles` ([#480](https://github.com/Polymer/lit-element/issues/480)). + +## [2.0.0-rc.4] - 2019-01-24 +### Added +* [Maintenance] Added script to publish dev releases automatically ([#476](https://github.com/Polymer/lit-element/pull/476)). +* Adds `unsafeCss` for composing "unsafe" values into `css`. Note, `CSSResult` is no longer constructable. ([#451](https://github.com/Polymer/lit-element/issues/451) and [#471](https://github.com/Polymer/lit-element/issues/471)). + +### Fixed +* Fixed a bug where we broke compatibility with closure compiler's property renaming optimizations. JSCompiler_renameProperty can't be a module export ([#465](https://github.com/Polymer/lit-element/pull/465)). +* Fixed an issue with inheriting from `styles` property when extending a superclass that is never instanced. ([#470](https://github.com/Polymer/lit-element/pull/470)). +* Fixed an issue with Closure Compiler and ([#470](https://github.com/Polymer/lit-element/pull/470)) ([#476](https://github.com/Polymer/lit-element/pull/476)). + +## [2.0.0-rc.3] - 2019-01-18 +### Fixed +* README: Fixed jsfiddle reference ([#435](https://github.com/Polymer/lit-element/pull/435)). +* Compile with Closure Compiler cleanly ([#436](https://github.com/Polymer/lit-element/pull/436)). +* Opt `@property` decorators out of Closure Compiler renaming ([#448](https://github.com/Polymer/lit-element/pull/448)). + +### Changed +* [Breaking] Property accessors are no longer wrapped when they already exist. Instead the `noAccessor` flag should be set when a user-defined accessor exists on the prototype (and in this case, user-defined accessors must call `requestUpdate` themselves). ([#454](https://github.com/Polymer/lit-element/pull/454)). +* Class fields can now be used to define styles, e.g. `static styles = css` and `styles` correctly compose when elements are extended ([#456](https://github.com/Polymer/lit-element/pull/456)). +* Styles returned via `static styles` are automatically flattend ([#437](https://github.com/Polymer/lit-element/pull/437)). +* Replace use of for/of loops over Maps with forEach ([#455](https://github.com/Polymer/lit-element/pull/455)) + +## [2.0.0-rc.2] - 2019-01-11 +### Fixed +* Fix references to `@polymer/lit-element` in README and docs ([#427](https://github.com/Polymer/lit-element/pull/427)). +* Fix decorator types causing compiler errors for TypeScript users. ([#431](https://github.com/Polymer/lit-element/pull/431)). + +## [2.0.0-rc.1] - 2019-01-10 +### Changed +* [Breaking] Changed NPM package name to `lit-element` + +## [0.7.0] - 2019-01-10 +### Added +* Updated decorator implementations to support TC39 decorator API proposal (supported by Babel 7.1+) in addition to the legacy decorator API (supported by older Babel and TypeScript) ([#156](https://github.com/Polymer/lit-element/issues/156)). +* Added `static get styles()` to allow defining element styling separate from `render` method. +This takes advantage of [`adoptedStyleSheets`](https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets) when possible ([#391](https://github.com/Polymer/lit-element/issues/391)). +* Added the `performUpdate` method to allow control of update timing ([#290](https://github.com/Polymer/lit-element/issues/290)). +* Updates deferred until first connection ([#258](https://github.com/Polymer/lit-element/issues/258)). +* Export `TemplateResult` and `SVGTemplateResult` ([#415](https://github.com/Polymer/lit-element/pull/415)). +### Changed +* [Breaking] The `createRenderRoot` method has moved from `UpdatingElement` to `LitElement`. Therefore, `UpdatingElement` no longer creates a `shadowRoot` by default ([#391](https://github.com/Polymer/lit-element/issues/391)). +* [Breaking] Changes property options to add `converter`. This option works the same as the previous `type` option except that the `converter` methods now also get `type` as the second argument. This effectively changes `type` to be a hint for the `converter`. A default `converter` is used if none is provided and it now supports `Boolean`, `String`, `Number`, `Object`, and `Array` ([#264](https://github.com/Polymer/lit-element/issues/264)). +* [Breaking] Numbers and strings now become null if their reflected attribute is removed (https://github.com/Polymer/lit-element/issues/264)). +* [Breaking] Previously, when an attribute changed as a result of a reflecting property changing, the property was prevented from mutating again as can happen when a custom +`converter` is used. Now, the oppose is also true. When a property changes as a result of an attribute changing, the attribute is prevented from mutating again (https://github.com/Polymer/lit-element/issues/264)) +### Fixed +* [Breaking] User defined accessors are now wrapped to enable better composition ([#286](https://github.com/Polymer/lit-element/issues/286)) +* Type for `eventOptions` decorator now properly includes `passive` and `once` options ([#325](https://github.com/Polymer/lit-element/issues/325)) + +## [0.6.5] - 2018-12-13 +### Changed: +* Use lit-html 1.0 release candidate. + +### Fixed +* Types for the `property` and `customElement` decorators updated ([#288](https://github.com/Polymer/lit-element/issues/288) and [#291](https://github.com/Polymer/lit-element/issues/291)). +* Docs updated. + +## [0.6.4] - 2018-11-30 +### Changed +* Update lit-html dependency to ^0.14.0 ([#324](https://github.com/Polymer/lit-element/pull/324)). + +## [0.6.3] - 2018-11-08 +### Changed +* Update lit-html dependency to ^0.13.0 ([#298](https://github.com/Polymer/lit-element/pull/298)). + +## [0.6.2] - 2018-10-05 + +### Changed +* LitElement changed to a non-abstract class to be more compatible with the JavaScript mixin pattern +([#227](https://github.com/Polymer/lit-element/issues/227)). +* Update lit-html dependency to ^0.12.0 ([#244](https://github.com/Polymer/lit-element/pull/244)). +* Passes the component's `this` reference to lit-html as the `eventContext`, allowing unbound event listener methods ([#244](https://github.com/Polymer/lit-element/pull/244)). +### Added +* A `disconnectedCallback()` method was added to UpdatingElement ([#213](https://github.com/Polymer/lit-element/pull/213)). +* Added `@eventOptions()` decorator for setting event listener options on methods ([#244](https://github.com/Polymer/lit-element/pull/244)). + +## [0.6.1] - 2018-09-17 + +### Fixed +* Fixes part rendering and css custom properties issues introduced with lit-html 0.11.3 by updating to 0.11.4 (https://github.com/Polymer/lit-element/issues/202). + +### Removed +* Removed custom_typings for Polymer as they are no longer needed +(https://github.com/Polymer/lit-element/issues/186). + +## [0.6.0] - 2018-09-13 + +### Added +* Added `@query()`, `@queryAll()`, and `@customElement` decorators ([#159](https://github.com/Polymer/lit-element/pull/159)) + +### Changed +* Significantly changed update/render lifecycle and property API. Render lifecycle +is now `requestUpdate`, `shouldUpdate`, `update`, `render`, `firstUpdated` +(first time only), `updated`, `updateComplete`. Property options are now +`{attribute, reflect, type, hasChanged}`. Properties may be defined in a +`static get properties` or using the `@property` decorator. +(https://github.com/Polymer/lit-element/pull/132). + + +### Removed +* Removed render helpers `classString` and `styleString`. Similar directives +(`classMap` and `styleMap`) have been added to lit-html and should be used instead +(https://github.com/Polymer/lit-element/pull/165 and +https://github.com/Polymer/lit-html/pull/486). + +### Fixed +* The `npm run checksize` command should now return the correct minified size +(https://github.com/Polymer/lit-element/pull/153). +* The `firstUpdated` method should now always be called the first time the element +updates, even if `shouldUpdate` initially returned `false` +(https://github.com/Polymer/lit-element/pull/173). diff --git a/app/userland/app-stdlib/vendor/lit-element/CONTRIBUTING.md b/app/userland/app-stdlib/vendor/lit-element/CONTRIBUTING.md new file mode 100755 index 0000000000..c582ad7d33 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/CONTRIBUTING.md @@ -0,0 +1,143 @@ +# Contributing to Polymer + +There are many ways to contribute to the Polymer project! We welcome and truly appreciate contribution in all forms - issues and pull requests to the [main library](https://github.com/polymer/polymer), issues and pull requests to the [elements the Polymer team maintains](https://github.com/polymerelements), issues and pull requests to one of our many [Polymer-related tools](https://github.com/polymer), and of course we love to hear about any Polymer elements that you build to share with the community! + +## Logistics + +### Communicating with the Polymer team + +Beyond GitHub, we try to have a variety of different lines of communication open: + +* [Blog](https://blog.polymer-project.org/) +* [Twitter](https://twitter.com/polymer) +* [Google+ Community](https://plus.sandbox.google.com/u/0/communities/115626364525706131031?cfem=1) +* [Mailing list](https://groups.google.com/forum/#!forum/polymer-dev) +* [Slack channel](https://bit.ly/polymerslack) + +### The Polymer Repositories + +Because of the component-based nature of the Polymer project, we tend to have lots of different repositories. Our main repository for the Polymer library itself is at [github.com/Polymer/polymer](https://github.com/polymer/polymer). File any issues or pull requests that have to do with the core library on that repository, and we'll take a look ASAP. + +We keep all of the element "product lines" that the Polymer team maintains and distributes in the [PolymerElements](https://github.com/polymerelements) organization. For any element-specific issues or pull requests, file directly on the element's repository, such as the `paper-button` repository at [github.com/polymerelements/paper-button](https://github.com/polymerelements/paper-button). Of course, the elements built by the Polymer team are just a tiny fraction of all the Polymer-based elements out there - catalogs of other web components include [https://www.webcomponents.org/](https://github.com/webcomponents/webcomponents.org) and [component.kitchen](https://component.kitchen). + +The GoogleWebComponents element product line is maintained by teams all across Google, and so is kept in a separate organization: the [GoogleWebComponents](https://github.com/googlewebcomponents) org. Feel free to file issues and PR's on those elements directly in that organization. + +We also track each element product line overall in "meta-repos", named as `$PRODUCTLINE-elements`. These include [paper-elements](https://github.com/polymerelements/paper-elements), [iron-elements](https://github.com/polymerelements/iron-elements), [gold-elements](https://github.com/polymerelements/gold-elements), and more. Feel free to file issues for element requests on those meta-repos, and the README in each repo tracks a roadmap for the product line. + +### Contributor License Agreement + +You might notice our friendly CLA-bot commenting on a pull request you open if you haven't yet signed our CLA. We use the same CLA for all open-source Google projects, so you only have to sign it once. Once you complete the CLA, all your pull-requests will automatically get the `cla: yes` tag. + +If you've already signed a CLA but are still getting bothered by the awfully insistent CLA bot, it's possible we don't have your GitHub username or you're using a different email address. Check the [information on your CLA](https://cla.developers.google.com/clas) or see this help article on [setting the email on your git commits](https://help.github.com/articles/setting-your-email-in-git/). + +[Complete the CLA](https://cla.developers.google.com/clas) + +## Contributing + +### Contributing documentation + +Docs source is in the `docs` folder. To build the site yourself, see the instructions in [docs/README.md](docs/README.md). + +### Filing bugs + +The Polymer team heavily uses (and loves!) GitHub for all of our software management. We use GitHub issues to track all bugs and features. + +If you find an issue, please do file it on the repository. The [Polymer/polymer issues](https://github.com/polymer/polymer/issues) should be used only for issues on the Polymer library itself - bugs somewhere in the core codebase. + +For issues with elements the team maintains, please file directly on the element's repository. If you're not sure if a bug stems from the element or the library, air toward filing it on the element and we'll move the issue if necessary. + +Please file issues using the issue template provided, filling out as many fields as possible. We love examples for addressing issues - issues with a jsBin, Plunkr, jsFiddle, or glitch.me repro will be much easier for us to work on quickly. You can start with [this StackBlitz](https://stackblitz.com/edit/lit-element-example?file=index.js) which sets up the basics to demonstrate a lit-element. If you need your repro to run in IE11, you can start from [this glitch](https://glitch.com/edit/#!/hello-lit-element?path=index.html:1:0), which serves the source via polyserve for automatic transpilation, although you must sign up for a glitch.me account to ensure your code persists for more than 5 days (note the glitch.me _editing environment_ is not compatible with IE11, however the "live" view link of the running code should work). + +Occasionally we'll close issues if they appear stale or are too vague - please don't take this personally! Please feel free to re-open issues we've closed if there's something we've missed and they still need to be addressed. + +### Contributing Pull Requests + +PR's are even better than issues. We gladly accept community pull requests. In general across the core library and all of the elements, there are a few necessary steps before we can accept a pull request: + +- Open an issue describing the problem that you are looking to solve in your PR (if one is not already open), and your approach to solving it. This makes it easier to have a conversation around the best general approach for solving your problem, outside of the code itself. +- Sign the [CLA](https://cla.developers.google.com/clas), as described above. +- Fork the repo you're making the fix on to your own GitHub account. +- Code! +- Ideally, squash your commits into a single commit with a clear message of what the PR does. If it absolutely makes sense to keep multiple commits, that's OK - or perhaps consider making two separate PR's. +- **Include tests that test the range of behavior that changes with your PR.** If you PR fixes a bug, make sure your tests capture that bug. If your PR adds new behavior, make sure that behavior is fully tested. Every PR *must* include associated tests. (See [Unit tests](#unit-tests) for more.) +- Submit your PR, making sure it references the issue you created. +- If your PR fixes a bug, make sure the issue includes clear steps to reproduce the bug so we can test your fix. + +If you've completed all of these steps the core team will do its best to respond to the PR as soon as possible. + +#### Contributing Code to Elements + +Though the aim of the Polymer library is to allow lots of flexibility and not get in your way, we work to standardize our elements to make them as toolable and easy to maintain as possible. + +All elements should follow the [Polymer element style guide](https://www.polymer-project.org/3.0/docs/tools/documentation), which defines how to specify properties, documentation, and more. It's a great guide to follow when building your own elements as well, for maximum standardization and toolability. For instance, structuring elements following the style guide will ensure that they work with the [`iron-component-page`](https://github.com/polymerelements/iron-component-page) element, an incredibly easy way to turn any raw element directly into a documentation page. + +#### Contributing Code to the Polymer library + +We follow the most common JavaScript and HTML style guidelines for how we structure our code - in general, look at the code and you'll know how to contribute! If you'd like a bit more structure, the [Google JavaScript Styleguide](https://google.github.io/styleguide/javascriptguide.xml) is a good place to start. + +Polymer also participates in Google's [Patch Rewards Program](https://www.google.com/about/appsecurity/patch-rewards/), where you can earn cold, hard cash for qualifying security patches to the Polymer library. Visit the [patch rewards page](https://www.google.com/about/appsecurity/patch-rewards/) to find out more. + +## Unit tests + +All Polymer projects use [`polymer-cli`](https://github.com/Polymer/tools/tree/master/packages/cli) for unit tests. + +For maximum flexibility, install `polymer-cli` locally: + + npm install -g polymer-cli + +### Running the lit-element unit tests + +To run the lit-element unit tests: + +1. Clone the [lit-element repo](https://github.com/polymer/lit-element). + +2. Install the dependencies: + + npm install + +3. Run the tests: + + npm test + + Or if you have `polymer-cli` installed locally: + + polymer test --npm + +To run individual test suites: + +npm test path/to/suite + +Or: + +polymer test --npm path/to/suite + +For example: + + polymer test --npm test/index.html + +You can also run tests in the browser: + + polymer serve --npm + +Navigate to: + +[`http://localhost:8080/components/@polymer/lit-element/test/index.html`](http://localhost:8080/components/@polymer/lit-element/test/index.html) + +### Configuring `web-component-tester` + +By default, `polymer test` runs tests on all installed browsers. You can configure it +to run tests on a subset of available browsers, or to run tests remotely using Sauce Labs. + +See the [`web-component-tester` README](https://github.com/Polymer/tools/tree/master/packages/web-component-tester) for +information on configuring the tool using by `polymer-cli` to run the tests. + +### Viewing the source documentation locally + +You can view the updates you make to the source documentation locally with the following steps. +Make sure to rerun step 1 after every change you make. + +1. Run `polymer analyze > analysis.json` + +1. Run `polymer serve` + +1. Open `http://127.0.0.1:PORT/components/polymer/` to view the documentation diff --git a/app/userland/app-stdlib/vendor/lit-element/LICENSE b/app/userland/app-stdlib/vendor/lit-element/LICENSE new file mode 100755 index 0000000000..39cfe44304 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2017, The Polymer Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/app/userland/app-stdlib/vendor/lit-element/README.md b/app/userland/app-stdlib/vendor/lit-element/README.md new file mode 100755 index 0000000000..681cb5c3e1 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/README.md @@ -0,0 +1,85 @@ +# LitElement +A simple base class for creating fast, lightweight web components with [lit-html](https://lit-html.polymer-project.org/). + +[![Build Status](https://travis-ci.org/Polymer/lit-element.svg?branch=master)](https://travis-ci.org/Polymer/lit-element) +[![Published on npm](https://img.shields.io/npm/v/lit-element.svg)](https://www.npmjs.com/package/lit-element) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/lit-element) +[![Mentioned in Awesome lit-html](https://awesome.re/mentioned-badge.svg)](https://github.com/web-padawan/awesome-lit-html) + +## Documentation + +Full documentation is available at [lit-element.polymer-project.org](https://lit-element.polymer-project.org). + +## Overview + +LitElement uses [lit-html](https://lit-html.polymer-project.org/) to render into the +element's [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) +and adds API to help manage element properties and attributes. LitElement reacts to changes in properties +and renders declaratively using `lit-html`. See the [lit-html guide](https://lit-html.polymer-project.org/guide) +for additional information on how to create templates for lit-element. + +```ts + import {LitElement, html, css, customElement, property} from 'lit-element'; + + // This decorator defines the element. + @customElement('my-element'); + export class MyElement extends LitElement { + + // This decorator creates a property accessor that triggers rendering and + // an observed attribute. + @property() + mood = 'great'; + + static styles = css` + span { + color: green; + }`; + + // Render element DOM by returning a `lit-html` template. + render() { + return html`Web Components are ${this.mood}!`; + } + + } +``` + +```html + +``` + +Note, this example uses decorators to create properties. Decorators are a proposed +standard currently available in [TypeScript](https://www.typescriptlang.org/) or [Babel](https://babeljs.io/docs/en/babel-plugin-proposal-decorators). LitElement also supports a [vanilla JavaScript method](https://lit-element.polymer-project.org/guide/properties#declare) of declaring reactive properties. + +## Examples + + * Runs in all [supported](#supported-browsers) browsers: [Glitch](https://glitch.com/edit/#!/hello-lit-element?path=index.html) + + * Runs in browsers with [JavaScript Modules](https://caniuse.com/#search=modules): [Stackblitz](https://stackblitz.com/edit/lit-element-demo?file=src%2Fmy-element.js), [JSFiddle](https://jsfiddle.net/sorvell1/801f9cdu/), [JSBin](http://jsbin.com/vecuyan/edit?html,output), +[CodePen](https://codepen.io/sorvell/pen/RYQyoe?editors=1000). + + * You can also copy [this HTML file](https://gist.githubusercontent.com/sorvell/48f4b7be35c8748e8f6db5c66d36ee29/raw/67346e4e8bc4c81d5a7968d18f0a6a8bc00d792e/index.html) into a local file and run it in any browser that supports [JavaScript Modules]((https://caniuse.com/#search=modules)). + +## Installation + +From inside your project folder, run: + +```bash +$ npm install lit-element +``` + +To install the web components polyfills needed for older browsers: + +```bash +$ npm i -D @webcomponents/webcomponentsjs +``` + +## Supported Browsers + +The last 2 versions of all modern browsers are supported, including +Chrome, Safari, Opera, Firefox, Edge. In addition, Internet Explorer 11 is also supported. + +Edge and Internet Explorer 11 require the web components polyfills. + +## Contributing + +Please see [CONTRIBUTING.md](./CONTRIBUTING.md). \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lib/css-tag.js b/app/userland/app-stdlib/vendor/lit-element/lib/css-tag.js new file mode 100644 index 0000000000..cea2875bf9 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lib/css-tag.js @@ -0,0 +1,70 @@ +/** +@license +Copyright (c) 2019 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at +http://polymer.github.io/LICENSE.txt The complete set of authors may be found at +http://polymer.github.io/AUTHORS.txt The complete set of contributors may be +found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as +part of the polymer project is also subject to an additional IP rights grant +found at http://polymer.github.io/PATENTS.txt +*/ +export const supportsAdoptingStyleSheets = ('adoptedStyleSheets' in Document.prototype) && + ('replace' in CSSStyleSheet.prototype); +const constructionToken = Symbol(); +export class CSSResult { + constructor(cssText, safeToken) { + if (safeToken !== constructionToken) { + throw new Error('CSSResult is not constructable. Use `unsafeCSS` or `css` instead.'); + } + this.cssText = cssText; + } + // Note, this is a getter so that it's lazy. In practice, this means + // stylesheets are not created until the first element instance is made. + get styleSheet() { + if (this._styleSheet === undefined) { + // Note, if `adoptedStyleSheets` is supported then we assume CSSStyleSheet + // is constructable. + if (supportsAdoptingStyleSheets) { + this._styleSheet = new CSSStyleSheet(); + this._styleSheet.replaceSync(this.cssText); + } + else { + this._styleSheet = null; + } + } + return this._styleSheet; + } + toString() { + return this.cssText; + } +} +/** + * Wrap a value for interpolation in a css tagged template literal. + * + * This is unsafe because untrusted CSS text can be used to phone home + * or exfiltrate data to an attacker controlled site. Take care to only use + * this with trusted input. + */ +export const unsafeCSS = (value) => { + return new CSSResult(String(value), constructionToken); +}; +const textFromCSSResult = (value) => { + if (value instanceof CSSResult) { + return value.cssText; + } + else { + throw new Error(`Value passed to 'css' function must be a 'css' function result: ${value}. Use 'unsafeCSS' to pass non-literal values, but + take care to ensure page security.`); + } +}; +/** + * Template tag which which can be used with LitElement's `style` property to + * set element styles. For security reasons, only literal string values may be + * used. To incorporate non-literal values `unsafeCSS` may be used inside a + * template string part. + */ +export const css = (strings, ...values) => { + const cssText = values.reduce((acc, v, idx) => acc + textFromCSSResult(v) + strings[idx + 1], strings[0]); + return new CSSResult(cssText, constructionToken); +}; +//# sourceMappingURL=css-tag.js.map \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lib/css-tag.js.map b/app/userland/app-stdlib/vendor/lit-element/lib/css-tag.js.map new file mode 100644 index 0000000000..e82db9480d --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lib/css-tag.js.map @@ -0,0 +1 @@ +{"version":3,"file":"css-tag.js","sourceRoot":"","sources":["../src/lib/css-tag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;EASE;AAEF,MAAM,CAAC,MAAM,2BAA2B,GACpC,CAAC,oBAAoB,IAAI,QAAQ,CAAC,SAAS,CAAC;IAC5C,CAAC,SAAS,IAAI,aAAa,CAAC,SAAS,CAAC,CAAC;AAE3C,MAAM,iBAAiB,GAAG,MAAM,EAAE,CAAC;AAEnC,MAAM,OAAO,SAAS;IAKpB,YAAY,OAAe,EAAE,SAAiB;QAC5C,IAAI,SAAS,KAAK,iBAAiB,EAAE;YACnC,MAAM,IAAI,KAAK,CACX,mEAAmE,CAAC,CAAC;SAC1E;QACD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,oEAAoE;IACpE,wEAAwE;IACxE,IAAI,UAAU;QACZ,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,EAAE;YAClC,0EAA0E;YAC1E,oBAAoB;YACpB,IAAI,2BAA2B,EAAE;gBAC/B,IAAI,CAAC,WAAW,GAAG,IAAI,aAAa,EAAE,CAAC;gBACvC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;aAC5C;iBAAM;gBACL,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;aACzB;SACF;QACD,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAc,EAAE,EAAE;IAC1C,OAAO,IAAI,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC,CAAC;AACzD,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,CAAC,KAAgB,EAAE,EAAE;IAC7C,IAAI,KAAK,YAAY,SAAS,EAAE;QAC9B,OAAO,KAAK,CAAC,OAAO,CAAC;KACtB;SAAM;QACL,MAAM,IAAI,KAAK,CACX,mEACI,KAAK;+CAC8B,CAAC,CAAC;KAC9C;AACH,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,OAA6B,EAAE,GAAG,MAAmB,EAAE,EAAE;IAC3E,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CACzB,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,iBAAiB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,EAC9D,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,IAAI,SAAS,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;AACnD,CAAC,CAAC","sourcesContent":["/**\n@license\nCopyright (c) 2019 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\n\nexport const supportsAdoptingStyleSheets =\n ('adoptedStyleSheets' in Document.prototype) &&\n ('replace' in CSSStyleSheet.prototype);\n\nconst constructionToken = Symbol();\n\nexport class CSSResult {\n _styleSheet?: CSSStyleSheet|null;\n\n readonly cssText: string;\n\n constructor(cssText: string, safeToken: symbol) {\n if (safeToken !== constructionToken) {\n throw new Error(\n 'CSSResult is not constructable. Use `unsafeCSS` or `css` instead.');\n }\n this.cssText = cssText;\n }\n\n // Note, this is a getter so that it's lazy. In practice, this means\n // stylesheets are not created until the first element instance is made.\n get styleSheet(): CSSStyleSheet|null {\n if (this._styleSheet === undefined) {\n // Note, if `adoptedStyleSheets` is supported then we assume CSSStyleSheet\n // is constructable.\n if (supportsAdoptingStyleSheets) {\n this._styleSheet = new CSSStyleSheet();\n this._styleSheet.replaceSync(this.cssText);\n } else {\n this._styleSheet = null;\n }\n }\n return this._styleSheet;\n }\n\n toString(): String {\n return this.cssText;\n }\n}\n\n/**\n * Wrap a value for interpolation in a css tagged template literal.\n *\n * This is unsafe because untrusted CSS text can be used to phone home\n * or exfiltrate data to an attacker controlled site. Take care to only use\n * this with trusted input.\n */\nexport const unsafeCSS = (value: unknown) => {\n return new CSSResult(String(value), constructionToken);\n};\n\nconst textFromCSSResult = (value: CSSResult) => {\n if (value instanceof CSSResult) {\n return value.cssText;\n } else {\n throw new Error(\n `Value passed to 'css' function must be a 'css' function result: ${\n value}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`);\n }\n};\n\n/**\n * Template tag which which can be used with LitElement's `style` property to\n * set element styles. For security reasons, only literal string values may be\n * used. To incorporate non-literal values `unsafeCSS` may be used inside a\n * template string part.\n */\nexport const css = (strings: TemplateStringsArray, ...values: CSSResult[]) => {\n const cssText = values.reduce(\n (acc, v, idx) => acc + textFromCSSResult(v) + strings[idx + 1],\n strings[0]);\n return new CSSResult(cssText, constructionToken);\n};\n"]} \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lib/decorators.js b/app/userland/app-stdlib/vendor/lit-element/lib/decorators.js new file mode 100644 index 0000000000..b34faf1ddc --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lib/decorators.js @@ -0,0 +1,188 @@ +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ +const legacyCustomElement = (tagName, clazz) => { + window.customElements.define(tagName, clazz); + // Cast as any because TS doesn't recognize the return type as being a + // subtype of the decorated class when clazz is typed as + // `Constructor` for some reason. + // `Constructor` is helpful to make sure the decorator is + // applied to elements however. + // tslint:disable-next-line:no-any + return clazz; +}; +const standardCustomElement = (tagName, descriptor) => { + const { kind, elements } = descriptor; + return { + kind, + elements, + // This callback is called once the class is otherwise fully defined + finisher(clazz) { + window.customElements.define(tagName, clazz); + } + }; +}; +/** + * Class decorator factory that defines the decorated class as a custom element. + * + * @param tagName the name of the custom element to define + */ +export const customElement = (tagName) => (classOrDescriptor) => (typeof classOrDescriptor === 'function') ? + legacyCustomElement(tagName, classOrDescriptor) : + standardCustomElement(tagName, classOrDescriptor); +const standardProperty = (options, element) => { + // When decorating an accessor, pass it through and add property metadata. + // Note, the `hasOwnProperty` check in `createProperty` ensures we don't + // stomp over the user's accessor. + if (element.kind === 'method' && element.descriptor && + !('value' in element.descriptor)) { + return Object.assign({}, element, { finisher(clazz) { + clazz.createProperty(element.key, options); + } }); + } + else { + // createProperty() takes care of defining the property, but we still + // must return some kind of descriptor, so return a descriptor for an + // unused prototype field. The finisher calls createProperty(). + return { + kind: 'field', + key: Symbol(), + placement: 'own', + descriptor: {}, + // When @babel/plugin-proposal-decorators implements initializers, + // do this instead of the initializer below. See: + // https://github.com/babel/babel/issues/9260 extras: [ + // { + // kind: 'initializer', + // placement: 'own', + // initializer: descriptor.initializer, + // } + // ], + // tslint:disable-next-line:no-any decorator + initializer() { + if (typeof element.initializer === 'function') { + this[element.key] = element.initializer.call(this); + } + }, + finisher(clazz) { + clazz.createProperty(element.key, options); + } + }; + } +}; +const legacyProperty = (options, proto, name) => { + proto.constructor + .createProperty(name, options); +}; +/** + * A property decorator which creates a LitElement property which reflects a + * corresponding attribute value. A `PropertyDeclaration` may optionally be + * supplied to configure property features. + * + * @ExportDecoratedItems + */ +export function property(options) { + // tslint:disable-next-line:no-any decorator + return (protoOrDescriptor, name) => (name !== undefined) ? + legacyProperty(options, protoOrDescriptor, name) : + standardProperty(options, protoOrDescriptor); +} +/** + * A property decorator that converts a class property into a getter that + * executes a querySelector on the element's renderRoot. + */ +export const query = _query((target, selector) => target.querySelector(selector)); +/** + * A property decorator that converts a class property into a getter + * that executes a querySelectorAll on the element's renderRoot. + */ +export const queryAll = _query((target, selector) => target.querySelectorAll(selector)); +const legacyQuery = (descriptor, proto, name) => { + Object.defineProperty(proto, name, descriptor); +}; +const standardQuery = (descriptor, element) => ({ + kind: 'method', + placement: 'prototype', + key: element.key, + descriptor, +}); +/** + * Base-implementation of `@query` and `@queryAll` decorators. + * + * @param queryFn exectute a `selector` (ie, querySelector or querySelectorAll) + * against `target`. + * @suppress {visibility} The descriptor accesses an internal field on the + * element. + */ +function _query(queryFn) { + return (selector) => (protoOrDescriptor, + // tslint:disable-next-line:no-any decorator + name) => { + const descriptor = { + get() { + return queryFn(this.renderRoot, selector); + }, + enumerable: true, + configurable: true, + }; + return (name !== undefined) ? + legacyQuery(descriptor, protoOrDescriptor, name) : + standardQuery(descriptor, protoOrDescriptor); + }; +} +const standardEventOptions = (options, element) => { + return Object.assign({}, element, { finisher(clazz) { + Object.assign(clazz.prototype[element.key], options); + } }); +}; +const legacyEventOptions = +// tslint:disable-next-line:no-any legacy decorator +(options, proto, name) => { + Object.assign(proto[name], options); +}; +/** + * Adds event listener options to a method used as an event listener in a + * lit-html template. + * + * @param options An object that specifis event listener options as accepted by + * `EventTarget#addEventListener` and `EventTarget#removeEventListener`. + * + * Current browsers support the `capture`, `passive`, and `once` options. See: + * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters + * + * @example + * + * class MyElement { + * + * clicked = false; + * + * render() { + * return html`
`; + * } + * + * @eventOptions({capture: true}) + * _onClick(e) { + * this.clicked = true; + * } + * } + */ +export const eventOptions = (options) => +// Return value typed as any to prevent TypeScript from complaining that +// standard decorator function signature does not match TypeScript decorator +// signature +// TODO(kschaaf): unclear why it was only failing on this decorator and not +// the others +((protoOrDescriptor, name) => (name !== undefined) ? + legacyEventOptions(options, protoOrDescriptor, name) : + standardEventOptions(options, protoOrDescriptor)); +//# sourceMappingURL=decorators.js.map \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lib/decorators.js.map b/app/userland/app-stdlib/vendor/lit-element/lib/decorators.js.map new file mode 100644 index 0000000000..74531c6a6c --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lib/decorators.js.map @@ -0,0 +1 @@ +{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/lib/decorators.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;GAYG;AA4BH,MAAM,mBAAmB,GACrB,CAAC,OAAe,EAAE,KAA+B,EAAE,EAAE;IACnD,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC7C,sEAAsE;IACtE,wDAAwD;IACxD,8CAA8C;IAC9C,sEAAsE;IACtE,+BAA+B;IAC/B,kCAAkC;IAClC,OAAO,KAAY,CAAC;AACtB,CAAC,CAAC;AAEN,MAAM,qBAAqB,GACvB,CAAC,OAAe,EAAE,UAA2B,EAAE,EAAE;IAC/C,MAAM,EAAC,IAAI,EAAE,QAAQ,EAAC,GAAG,UAAU,CAAC;IACpC,OAAO;QACL,IAAI;QACJ,QAAQ;QACR,oEAAoE;QACpE,QAAQ,CAAC,KAA+B;YACtC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC/C,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEN;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,OAAe,EAAE,EAAE,CAC7C,CAAC,iBAA2D,EAAE,EAAE,CAC5D,CAAC,OAAO,iBAAiB,KAAK,UAAU,CAAC,CAAC,CAAC;IAC/C,mBAAmB,CACf,OAAO,EAAE,iBAA6C,CAAC,CAAC,CAAC;IAC7D,qBAAqB,CAAC,OAAO,EAAE,iBAAoC,CAAC,CAAC;AAEzE,MAAM,gBAAgB,GAClB,CAAC,OAA4B,EAAE,OAAqB,EAAE,EAAE;IACtD,0EAA0E;IAC1E,wEAAwE;IACxE,kCAAkC;IAClC,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,UAAU;QAC/C,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,EAAE;QACpC,yBACK,OAAO,IACV,QAAQ,CAAC,KAA6B;gBACpC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC7C,CAAC,IACD;KACH;SAAM;QACL,qEAAqE;QACrE,qEAAqE;QACrE,+DAA+D;QAC/D,OAAO;YACL,IAAI,EAAE,OAAO;YACb,GAAG,EAAE,MAAM,EAAE;YACb,SAAS,EAAE,KAAK;YAChB,UAAU,EAAE,EAAE;YACd,kEAAkE;YAClE,iDAAiD;YACjD,uDAAuD;YACvD,MAAM;YACN,2BAA2B;YAC3B,wBAAwB;YACxB,2CAA2C;YAC3C,MAAM;YACN,KAAK;YACL,4CAA4C;YAC5C,WAAW;gBACT,IAAI,OAAO,OAAO,CAAC,WAAW,KAAK,UAAU,EAAE;oBAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,WAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;iBACrD;YACH,CAAC;YACD,QAAQ,CAAC,KAA6B;gBACpC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC7C,CAAC;SACF,CAAC;KACH;AACH,CAAC,CAAC;AAEN,MAAM,cAAc,GAChB,CAAC,OAA4B,EAAE,KAAa,EAAE,IAAiB,EAAE,EAAE;IAChE,KAAK,CAAC,WAAsC;SACxC,cAAc,CAAC,IAAK,EAAE,OAAO,CAAC,CAAC;AACtC,CAAC,CAAC;AAEN;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,OAA6B;IACpD,4CAA4C;IAC5C,OAAO,CAAC,iBAAsC,EAAE,IAAkB,EAAO,EAAE,CAChE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC;QAC7B,cAAc,CAAC,OAAQ,EAAE,iBAA2B,EAAE,IAAI,CAAC,CAAC,CAAC;QAC7D,gBAAgB,CAAC,OAAQ,EAAE,iBAAiC,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CACvB,CAAC,MAAoB,EAAE,QAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAC1B,CAAC,MAAoB,EAAE,QAAgB,EAAE,EAAE,CACvC,MAAM,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE3C,MAAM,WAAW,GACb,CAAC,UAA8B,EAAE,KAAa,EAAE,IAAiB,EAAE,EAAE;IACnE,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;AACjD,CAAC,CAAC;AAEN,MAAM,aAAa,GAAG,CAAC,UAA8B,EAAE,OAAqB,EAAE,EAAE,CAC5E,CAAC;IACC,IAAI,EAAE,QAAQ;IACd,SAAS,EAAE,WAAW;IACtB,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,UAAU;CACX,CAAC,CAAC;AAEP;;;;;;;GAOG;AACH,SAAS,MAAM,CAAI,OAAsD;IACvE,OAAO,CAAC,QAAgB,EAAE,EAAE,CACjB,CAAC,iBAAsC;IACtC,4CAA4C;IAC5C,IAAkB,EAAO,EAAE;QAC1B,MAAM,UAAU,GAAG;YACjB,GAAG;gBACD,OAAO,OAAO,CAAC,IAAI,CAAC,UAAW,EAAE,QAAQ,CAAC,CAAC;YAC7C,CAAC;YACD,UAAU,EAAE,IAAI;YAChB,YAAY,EAAE,IAAI;SACnB,CAAC;QACF,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC;YACzB,WAAW,CAAC,UAAU,EAAE,iBAA2B,EAAE,IAAI,CAAC,CAAC,CAAC;YAC5D,aAAa,CAAC,UAAU,EAAE,iBAAiC,CAAC,CAAC;IACnE,CAAC,CAAC;AACf,CAAC;AAED,MAAM,oBAAoB,GACtB,CAAC,OAAgC,EAAE,OAAqB,EAAE,EAAE;IAC1D,yBACK,OAAO,IACV,QAAQ,CAAC,KAA6B;YACpC,MAAM,CAAC,MAAM,CACT,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,GAA4B,CAAC,EAAE,OAAO,CAAC,CAAC;QACtE,CAAC,IACD;AACJ,CAAC,CAAC;AAEN,MAAM,kBAAkB;AACpB,mDAAmD;AACnD,CAAC,OAAgC,EAAE,KAAU,EAAE,IAAiB,EAAE,EAAE;IAClE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AACtC,CAAC,CAAC;AAEN;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAgC,EAAE,EAAE;AAC7D,wEAAwE;AACxE,4EAA4E;AAC5E,YAAY;AACZ,2EAA2E;AAC3E,aAAa;AACb,CAAC,CAAC,iBAAsC,EAAE,IAAa,EAAE,EAAE,CACtD,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC;IACtB,kBAAkB,CAAC,OAAO,EAAE,iBAA2B,EAAE,IAAI,CAAC,CAAC,CAAC;IAChE,oBAAoB,CAAC,OAAO,EAAE,iBAAiC,CAAC,CAE9D,CAAC","sourcesContent":["\n/**\n * @license\n * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.\n * This code may only be used under the BSD style license found at\n * http://polymer.github.io/LICENSE.txt\n * The complete set of authors may be found at\n * http://polymer.github.io/AUTHORS.txt\n * The complete set of contributors may be found at\n * http://polymer.github.io/CONTRIBUTORS.txt\n * Code distributed by Google as part of the polymer project is also\n * subject to an additional IP rights grant found at\n * http://polymer.github.io/PATENTS.txt\n */\n\nimport {LitElement} from '../lit-element.js';\n\nimport {PropertyDeclaration, UpdatingElement} from './updating-element.js';\n\nexport type Constructor = {\n new (...args: unknown[]): T\n};\n\n// From the TC39 Decorators proposal\ninterface ClassDescriptor {\n kind: 'class';\n elements: ClassElement[];\n finisher?: (clazz: Constructor) => undefined | Constructor;\n}\n\n// From the TC39 Decorators proposal\ninterface ClassElement {\n kind: 'field'|'method';\n key: PropertyKey;\n placement: 'static'|'prototype'|'own';\n initializer?: Function;\n extras?: ClassElement[];\n finisher?: (clazz: Constructor) => undefined | Constructor;\n descriptor?: PropertyDescriptor;\n}\n\nconst legacyCustomElement =\n (tagName: string, clazz: Constructor) => {\n window.customElements.define(tagName, clazz);\n // Cast as any because TS doesn't recognize the return type as being a\n // subtype of the decorated class when clazz is typed as\n // `Constructor` for some reason.\n // `Constructor` is helpful to make sure the decorator is\n // applied to elements however.\n // tslint:disable-next-line:no-any\n return clazz as any;\n };\n\nconst standardCustomElement =\n (tagName: string, descriptor: ClassDescriptor) => {\n const {kind, elements} = descriptor;\n return {\n kind,\n elements,\n // This callback is called once the class is otherwise fully defined\n finisher(clazz: Constructor) {\n window.customElements.define(tagName, clazz);\n }\n };\n };\n\n/**\n * Class decorator factory that defines the decorated class as a custom element.\n *\n * @param tagName the name of the custom element to define\n */\nexport const customElement = (tagName: string) =>\n (classOrDescriptor: Constructor|ClassDescriptor) =>\n (typeof classOrDescriptor === 'function') ?\n legacyCustomElement(\n tagName, classOrDescriptor as Constructor) :\n standardCustomElement(tagName, classOrDescriptor as ClassDescriptor);\n\nconst standardProperty =\n (options: PropertyDeclaration, element: ClassElement) => {\n // When decorating an accessor, pass it through and add property metadata.\n // Note, the `hasOwnProperty` check in `createProperty` ensures we don't\n // stomp over the user's accessor.\n if (element.kind === 'method' && element.descriptor &&\n !('value' in element.descriptor)) {\n return {\n ...element,\n finisher(clazz: typeof UpdatingElement) {\n clazz.createProperty(element.key, options);\n }\n };\n } else {\n // createProperty() takes care of defining the property, but we still\n // must return some kind of descriptor, so return a descriptor for an\n // unused prototype field. The finisher calls createProperty().\n return {\n kind: 'field',\n key: Symbol(),\n placement: 'own',\n descriptor: {},\n // When @babel/plugin-proposal-decorators implements initializers,\n // do this instead of the initializer below. See:\n // https://github.com/babel/babel/issues/9260 extras: [\n // {\n // kind: 'initializer',\n // placement: 'own',\n // initializer: descriptor.initializer,\n // }\n // ],\n // tslint:disable-next-line:no-any decorator\n initializer(this: any) {\n if (typeof element.initializer === 'function') {\n this[element.key] = element.initializer!.call(this);\n }\n },\n finisher(clazz: typeof UpdatingElement) {\n clazz.createProperty(element.key, options);\n }\n };\n }\n };\n\nconst legacyProperty =\n (options: PropertyDeclaration, proto: Object, name: PropertyKey) => {\n (proto.constructor as typeof UpdatingElement)\n .createProperty(name!, options);\n };\n\n/**\n * A property decorator which creates a LitElement property which reflects a\n * corresponding attribute value. A `PropertyDeclaration` may optionally be\n * supplied to configure property features.\n *\n * @ExportDecoratedItems\n */\nexport function property(options?: PropertyDeclaration) {\n // tslint:disable-next-line:no-any decorator\n return (protoOrDescriptor: Object|ClassElement, name?: PropertyKey): any =>\n (name !== undefined) ?\n legacyProperty(options!, protoOrDescriptor as Object, name) :\n standardProperty(options!, protoOrDescriptor as ClassElement);\n}\n\n/**\n * A property decorator that converts a class property into a getter that\n * executes a querySelector on the element's renderRoot.\n */\nexport const query = _query(\n (target: NodeSelector, selector: string) => target.querySelector(selector));\n\n/**\n * A property decorator that converts a class property into a getter\n * that executes a querySelectorAll on the element's renderRoot.\n */\nexport const queryAll = _query(\n (target: NodeSelector, selector: string) =>\n target.querySelectorAll(selector));\n\nconst legacyQuery =\n (descriptor: PropertyDescriptor, proto: Object, name: PropertyKey) => {\n Object.defineProperty(proto, name, descriptor);\n };\n\nconst standardQuery = (descriptor: PropertyDescriptor, element: ClassElement) =>\n ({\n kind: 'method',\n placement: 'prototype',\n key: element.key,\n descriptor,\n });\n\n/**\n * Base-implementation of `@query` and `@queryAll` decorators.\n *\n * @param queryFn exectute a `selector` (ie, querySelector or querySelectorAll)\n * against `target`.\n * @suppress {visibility} The descriptor accesses an internal field on the\n * element.\n */\nfunction _query(queryFn: (target: NodeSelector, selector: string) => T) {\n return (selector: string) =>\n (protoOrDescriptor: Object|ClassElement,\n // tslint:disable-next-line:no-any decorator\n name?: PropertyKey): any => {\n const descriptor = {\n get(this: LitElement) {\n return queryFn(this.renderRoot!, selector);\n },\n enumerable: true,\n configurable: true,\n };\n return (name !== undefined) ?\n legacyQuery(descriptor, protoOrDescriptor as Object, name) :\n standardQuery(descriptor, protoOrDescriptor as ClassElement);\n };\n}\n\nconst standardEventOptions =\n (options: AddEventListenerOptions, element: ClassElement) => {\n return {\n ...element,\n finisher(clazz: typeof UpdatingElement) {\n Object.assign(\n clazz.prototype[element.key as keyof UpdatingElement], options);\n }\n };\n };\n\nconst legacyEventOptions =\n // tslint:disable-next-line:no-any legacy decorator\n (options: AddEventListenerOptions, proto: any, name: PropertyKey) => {\n Object.assign(proto[name], options);\n };\n\n/**\n * Adds event listener options to a method used as an event listener in a\n * lit-html template.\n *\n * @param options An object that specifis event listener options as accepted by\n * `EventTarget#addEventListener` and `EventTarget#removeEventListener`.\n *\n * Current browsers support the `capture`, `passive`, and `once` options. See:\n * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters\n *\n * @example\n *\n * class MyElement {\n *\n * clicked = false;\n *\n * render() {\n * return html`
`;\n * }\n *\n * @eventOptions({capture: true})\n * _onClick(e) {\n * this.clicked = true;\n * }\n * }\n */\nexport const eventOptions = (options: AddEventListenerOptions) =>\n // Return value typed as any to prevent TypeScript from complaining that\n // standard decorator function signature does not match TypeScript decorator\n // signature\n // TODO(kschaaf): unclear why it was only failing on this decorator and not\n // the others\n ((protoOrDescriptor: Object|ClassElement, name?: string) =>\n (name !== undefined) ?\n legacyEventOptions(options, protoOrDescriptor as Object, name) :\n standardEventOptions(options, protoOrDescriptor as ClassElement)) as\n // tslint:disable-next-line:no-any decorator\n any;\n"]} \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lib/updating-element.js b/app/userland/app-stdlib/vendor/lit-element/lib/updating-element.js new file mode 100644 index 0000000000..af690d0f8d --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lib/updating-element.js @@ -0,0 +1,560 @@ +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ +/** + * When using Closure Compiler, JSCompiler_renameProperty(property, object) is + * replaced at compile time by the munged name for object[property]. We cannot + * alias this function, so we have to use a small shim that has the same + * behavior when not compiling. + */ +window.JSCompiler_renameProperty = + (prop, _obj) => prop; +export const defaultConverter = { + toAttribute(value, type) { + switch (type) { + case Boolean: + return value ? '' : null; + case Object: + case Array: + // if the value is `null` or `undefined` pass this through + // to allow removing/no change behavior. + return value == null ? value : JSON.stringify(value); + } + return value; + }, + fromAttribute(value, type) { + switch (type) { + case Boolean: + return value !== null; + case Number: + return value === null ? null : Number(value); + case Object: + case Array: + return JSON.parse(value); + } + return value; + } +}; +/** + * Change function that returns true if `value` is different from `oldValue`. + * This method is used as the default for a property's `hasChanged` function. + */ +export const notEqual = (value, old) => { + // This ensures (old==NaN, value==NaN) always returns false + return old !== value && (old === old || value === value); +}; +const defaultPropertyDeclaration = { + attribute: true, + type: String, + converter: defaultConverter, + reflect: false, + hasChanged: notEqual +}; +const microtaskPromise = Promise.resolve(true); +const STATE_HAS_UPDATED = 1; +const STATE_UPDATE_REQUESTED = 1 << 2; +const STATE_IS_REFLECTING_TO_ATTRIBUTE = 1 << 3; +const STATE_IS_REFLECTING_TO_PROPERTY = 1 << 4; +const STATE_HAS_CONNECTED = 1 << 5; +/** + * Base element class which manages element properties and attributes. When + * properties change, the `update` method is asynchronously called. This method + * should be supplied by subclassers to render updates as desired. + */ +export class UpdatingElement extends HTMLElement { + constructor() { + super(); + this._updateState = 0; + this._instanceProperties = undefined; + this._updatePromise = microtaskPromise; + this._hasConnectedResolver = undefined; + /** + * Map with keys for any properties that have changed since the last + * update cycle with previous values. + */ + this._changedProperties = new Map(); + /** + * Map with keys of properties that should be reflected when updated. + */ + this._reflectingProperties = undefined; + this.initialize(); + } + /** + * Returns a list of attributes corresponding to the registered properties. + * @nocollapse + */ + static get observedAttributes() { + // note: piggy backing on this to ensure we're finalized. + this.finalize(); + const attributes = []; + // Use forEach so this works even if for/of loops are compiled to for loops + // expecting arrays + this._classProperties.forEach((v, p) => { + const attr = this._attributeNameForProperty(p, v); + if (attr !== undefined) { + this._attributeToPropertyMap.set(attr, p); + attributes.push(attr); + } + }); + return attributes; + } + /** + * Ensures the private `_classProperties` property metadata is created. + * In addition to `finalize` this is also called in `createProperty` to + * ensure the `@property` decorator can add property metadata. + */ + /** @nocollapse */ + static _ensureClassProperties() { + // ensure private storage for property declarations. + if (!this.hasOwnProperty(JSCompiler_renameProperty('_classProperties', this))) { + this._classProperties = new Map(); + // NOTE: Workaround IE11 not supporting Map constructor argument. + const superProperties = Object.getPrototypeOf(this)._classProperties; + if (superProperties !== undefined) { + superProperties.forEach((v, k) => this._classProperties.set(k, v)); + } + } + } + /** + * Creates a property accessor on the element prototype if one does not exist. + * The property setter calls the property's `hasChanged` property option + * or uses a strict identity check to determine whether or not to request + * an update. + * @nocollapse + */ + static createProperty(name, options = defaultPropertyDeclaration) { + // Note, since this can be called by the `@property` decorator which + // is called before `finalize`, we ensure storage exists for property + // metadata. + this._ensureClassProperties(); + this._classProperties.set(name, options); + // Do not generate an accessor if the prototype already has one, since + // it would be lost otherwise and that would never be the user's intention; + // Instead, we expect users to call `requestUpdate` themselves from + // user-defined accessors. Note that if the super has an accessor we will + // still overwrite it + if (options.noAccessor || this.prototype.hasOwnProperty(name)) { + return; + } + const key = typeof name === 'symbol' ? Symbol() : `__${name}`; + Object.defineProperty(this.prototype, name, { + // tslint:disable-next-line:no-any no symbol in index + get() { + // tslint:disable-next-line:no-any no symbol in index + return this[key]; + }, + set(value) { + // tslint:disable-next-line:no-any no symbol in index + const oldValue = this[name]; + // tslint:disable-next-line:no-any no symbol in index + this[key] = value; + this.requestUpdate(name, oldValue); + }, + configurable: true, + enumerable: true + }); + } + /** + * Creates property accessors for registered properties and ensures + * any superclasses are also finalized. + * @nocollapse + */ + static finalize() { + if (this.hasOwnProperty(JSCompiler_renameProperty('finalized', this)) && + this.finalized) { + return; + } + // finalize any superclasses + const superCtor = Object.getPrototypeOf(this); + if (typeof superCtor.finalize === 'function') { + superCtor.finalize(); + } + this.finalized = true; + this._ensureClassProperties(); + // initialize Map populated in observedAttributes + this._attributeToPropertyMap = new Map(); + // make any properties + // Note, only process "own" properties since this element will inherit + // any properties defined on the superClass, and finalization ensures + // the entire prototype chain is finalized. + if (this.hasOwnProperty(JSCompiler_renameProperty('properties', this))) { + const props = this.properties; + // support symbols in properties (IE11 does not support this) + const propKeys = [ + ...Object.getOwnPropertyNames(props), + ...(typeof Object.getOwnPropertySymbols === 'function') ? + Object.getOwnPropertySymbols(props) : + [] + ]; + // This for/of is ok because propKeys is an array + for (const p of propKeys) { + // note, use of `any` is due to TypeSript lack of support for symbol in + // index types + // tslint:disable-next-line:no-any no symbol in index + this.createProperty(p, props[p]); + } + } + } + /** + * Returns the property name for the given attribute `name`. + * @nocollapse + */ + static _attributeNameForProperty(name, options) { + const attribute = options.attribute; + return attribute === false ? + undefined : + (typeof attribute === 'string' ? + attribute : + (typeof name === 'string' ? name.toLowerCase() : undefined)); + } + /** + * Returns true if a property should request an update. + * Called when a property value is set and uses the `hasChanged` + * option for the property if present or a strict identity check. + * @nocollapse + */ + static _valueHasChanged(value, old, hasChanged = notEqual) { + return hasChanged(value, old); + } + /** + * Returns the property value for the given attribute value. + * Called via the `attributeChangedCallback` and uses the property's + * `converter` or `converter.fromAttribute` property option. + * @nocollapse + */ + static _propertyValueFromAttribute(value, options) { + const type = options.type; + const converter = options.converter || defaultConverter; + const fromAttribute = (typeof converter === 'function' ? converter : converter.fromAttribute); + return fromAttribute ? fromAttribute(value, type) : value; + } + /** + * Returns the attribute value for the given property value. If this + * returns undefined, the property will *not* be reflected to an attribute. + * If this returns null, the attribute will be removed, otherwise the + * attribute will be set to the value. + * This uses the property's `reflect` and `type.toAttribute` property options. + * @nocollapse + */ + static _propertyValueToAttribute(value, options) { + if (options.reflect === undefined) { + return; + } + const type = options.type; + const converter = options.converter; + const toAttribute = converter && converter.toAttribute || + defaultConverter.toAttribute; + return toAttribute(value, type); + } + /** + * Performs element initialization. By default captures any pre-set values for + * registered properties. + */ + initialize() { + this._saveInstanceProperties(); + } + /** + * Fixes any properties set on the instance before upgrade time. + * Otherwise these would shadow the accessor and break these properties. + * The properties are stored in a Map which is played back after the + * constructor runs. Note, on very old versions of Safari (<=9) or Chrome + * (<=41), properties created for native platform properties like (`id` or + * `name`) may not have default values set in the element constructor. On + * these browsers native properties appear on instances and therefore their + * default value will overwrite any element default (e.g. if the element sets + * this.id = 'id' in the constructor, the 'id' will become '' since this is + * the native platform default). + */ + _saveInstanceProperties() { + // Use forEach so this works even if for/of loops are compiled to for loops + // expecting arrays + this.constructor + ._classProperties.forEach((_v, p) => { + if (this.hasOwnProperty(p)) { + const value = this[p]; + delete this[p]; + if (!this._instanceProperties) { + this._instanceProperties = new Map(); + } + this._instanceProperties.set(p, value); + } + }); + } + /** + * Applies previously saved instance properties. + */ + _applyInstanceProperties() { + // Use forEach so this works even if for/of loops are compiled to for loops + // expecting arrays + // tslint:disable-next-line:no-any + this._instanceProperties.forEach((v, p) => this[p] = v); + this._instanceProperties = undefined; + } + connectedCallback() { + this._updateState = this._updateState | STATE_HAS_CONNECTED; + // Ensure connection triggers an update. Updates cannot complete before + // connection and if one is pending connection the `_hasConnectionResolver` + // will exist. If so, resolve it to complete the update, otherwise + // requestUpdate. + if (this._hasConnectedResolver) { + this._hasConnectedResolver(); + this._hasConnectedResolver = undefined; + } + else { + this.requestUpdate(); + } + } + /** + * Allows for `super.disconnectedCallback()` in extensions while + * reserving the possibility of making non-breaking feature additions + * when disconnecting at some point in the future. + */ + disconnectedCallback() { + } + /** + * Synchronizes property values when attributes change. + */ + attributeChangedCallback(name, old, value) { + if (old !== value) { + this._attributeToProperty(name, value); + } + } + _propertyToAttribute(name, value, options = defaultPropertyDeclaration) { + const ctor = this.constructor; + const attr = ctor._attributeNameForProperty(name, options); + if (attr !== undefined) { + const attrValue = ctor._propertyValueToAttribute(value, options); + // an undefined value does not change the attribute. + if (attrValue === undefined) { + return; + } + // Track if the property is being reflected to avoid + // setting the property again via `attributeChangedCallback`. Note: + // 1. this takes advantage of the fact that the callback is synchronous. + // 2. will behave incorrectly if multiple attributes are in the reaction + // stack at time of calling. However, since we process attributes + // in `update` this should not be possible (or an extreme corner case + // that we'd like to discover). + // mark state reflecting + this._updateState = this._updateState | STATE_IS_REFLECTING_TO_ATTRIBUTE; + if (attrValue == null) { + this.removeAttribute(attr); + } + else { + this.setAttribute(attr, attrValue); + } + // mark state not reflecting + this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_ATTRIBUTE; + } + } + _attributeToProperty(name, value) { + // Use tracking info to avoid deserializing attribute value if it was + // just set from a property setter. + if (this._updateState & STATE_IS_REFLECTING_TO_ATTRIBUTE) { + return; + } + const ctor = this.constructor; + const propName = ctor._attributeToPropertyMap.get(name); + if (propName !== undefined) { + const options = ctor._classProperties.get(propName) || defaultPropertyDeclaration; + // mark state reflecting + this._updateState = this._updateState | STATE_IS_REFLECTING_TO_PROPERTY; + this[propName] = + // tslint:disable-next-line:no-any + ctor._propertyValueFromAttribute(value, options); + // mark state not reflecting + this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_PROPERTY; + } + } + /** + * Requests an update which is processed asynchronously. This should + * be called when an element should update based on some state not triggered + * by setting a property. In this case, pass no arguments. It should also be + * called when manually implementing a property setter. In this case, pass the + * property `name` and `oldValue` to ensure that any configured property + * options are honored. Returns the `updateComplete` Promise which is resolved + * when the update completes. + * + * @param name {PropertyKey} (optional) name of requesting property + * @param oldValue {any} (optional) old value of requesting property + * @returns {Promise} A Promise that is resolved when the update completes. + */ + requestUpdate(name, oldValue) { + let shouldRequestUpdate = true; + // if we have a property key, perform property update steps. + if (name !== undefined && !this._changedProperties.has(name)) { + const ctor = this.constructor; + const options = ctor._classProperties.get(name) || defaultPropertyDeclaration; + if (ctor._valueHasChanged(this[name], oldValue, options.hasChanged)) { + // track old value when changing. + this._changedProperties.set(name, oldValue); + // add to reflecting properties set + if (options.reflect === true && + !(this._updateState & STATE_IS_REFLECTING_TO_PROPERTY)) { + if (this._reflectingProperties === undefined) { + this._reflectingProperties = new Map(); + } + this._reflectingProperties.set(name, options); + } + // abort the request if the property should not be considered changed. + } + else { + shouldRequestUpdate = false; + } + } + if (!this._hasRequestedUpdate && shouldRequestUpdate) { + this._enqueueUpdate(); + } + return this.updateComplete; + } + /** + * Sets up the element to asynchronously update. + */ + async _enqueueUpdate() { + // Mark state updating... + this._updateState = this._updateState | STATE_UPDATE_REQUESTED; + let resolve; + const previousUpdatePromise = this._updatePromise; + this._updatePromise = new Promise((res) => resolve = res); + // Ensure any previous update has resolved before updating. + // This `await` also ensures that property changes are batched. + await previousUpdatePromise; + // Make sure the element has connected before updating. + if (!this._hasConnected) { + await new Promise((res) => this._hasConnectedResolver = res); + } + // Allow `performUpdate` to be asynchronous to enable scheduling of updates. + const result = this.performUpdate(); + // Note, this is to avoid delaying an additional microtask unless we need + // to. + if (result != null && + typeof result.then === 'function') { + await result; + } + resolve(!this._hasRequestedUpdate); + } + get _hasConnected() { + return (this._updateState & STATE_HAS_CONNECTED); + } + get _hasRequestedUpdate() { + return (this._updateState & STATE_UPDATE_REQUESTED); + } + get hasUpdated() { + return (this._updateState & STATE_HAS_UPDATED); + } + /** + * Performs an element update. + * + * You can override this method to change the timing of updates. For instance, + * to schedule updates to occur just before the next frame: + * + * ``` + * protected async performUpdate(): Promise { + * await new Promise((resolve) => requestAnimationFrame(() => resolve())); + * super.performUpdate(); + * } + * ``` + */ + performUpdate() { + // Mixin instance properties once, if they exist. + if (this._instanceProperties) { + this._applyInstanceProperties(); + } + if (this.shouldUpdate(this._changedProperties)) { + const changedProperties = this._changedProperties; + this.update(changedProperties); + this._markUpdated(); + if (!(this._updateState & STATE_HAS_UPDATED)) { + this._updateState = this._updateState | STATE_HAS_UPDATED; + this.firstUpdated(changedProperties); + } + this.updated(changedProperties); + } + else { + this._markUpdated(); + } + } + _markUpdated() { + this._changedProperties = new Map(); + this._updateState = this._updateState & ~STATE_UPDATE_REQUESTED; + } + /** + * Returns a Promise that resolves when the element has completed updating. + * The Promise value is a boolean that is `true` if the element completed the + * update without triggering another update. The Promise result is `false` if + * a property was set inside `updated()`. This getter can be implemented to + * await additional state. For example, it is sometimes useful to await a + * rendered element before fulfilling this Promise. To do this, first await + * `super.updateComplete` then any subsequent state. + * + * @returns {Promise} The Promise returns a boolean that indicates if the + * update resolved without triggering another update. + */ + get updateComplete() { + return this._updatePromise; + } + /** + * Controls whether or not `update` should be called when the element requests + * an update. By default, this method always returns `true`, but this can be + * customized to control when to update. + * + * * @param _changedProperties Map of changed properties with old values + */ + shouldUpdate(_changedProperties) { + return true; + } + /** + * Updates the element. This method reflects property values to attributes. + * It can be overridden to render and keep updated element DOM. + * Setting properties inside this method will *not* trigger + * another update. + * + * * @param _changedProperties Map of changed properties with old values + */ + update(_changedProperties) { + if (this._reflectingProperties !== undefined && + this._reflectingProperties.size > 0) { + // Use forEach so this works even if for/of loops are compiled to for + // loops expecting arrays + this._reflectingProperties.forEach((v, k) => this._propertyToAttribute(k, this[k], v)); + this._reflectingProperties = undefined; + } + } + /** + * Invoked whenever the element is updated. Implement to perform + * post-updating tasks via DOM APIs, for example, focusing an element. + * + * Setting properties inside this method will trigger the element to update + * again after this update cycle completes. + * + * * @param _changedProperties Map of changed properties with old values + */ + updated(_changedProperties) { + } + /** + * Invoked when the element is first updated. Implement to perform one time + * work on the element after update. + * + * Setting properties inside this method will trigger the element to update + * again after this update cycle completes. + * + * * @param _changedProperties Map of changed properties with old values + */ + firstUpdated(_changedProperties) { + } +} +/** + * Marks class as having finished creating properties. + */ +UpdatingElement.finalized = true; +//# sourceMappingURL=updating-element.js.map \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lib/updating-element.js.map b/app/userland/app-stdlib/vendor/lit-element/lib/updating-element.js.map new file mode 100644 index 0000000000..063d3156c8 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lib/updating-element.js.map @@ -0,0 +1 @@ +{"version":3,"file":"updating-element.js","sourceRoot":"","sources":["../src/lib/updating-element.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,yBAAyB;IAC5B,CAAwB,IAAO,EAAE,IAAa,EAAK,EAAE,CAAC,IAAI,CAAC;AA8G/D,MAAM,CAAC,MAAM,gBAAgB,GAA8B;IAEzD,WAAW,CAAC,KAAc,EAAE,IAAc;QACxC,QAAQ,IAAI,EAAE;YACZ,KAAK,OAAO;gBACV,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAC3B,KAAK,MAAM,CAAC;YACZ,KAAK,KAAK;gBACR,0DAA0D;gBAC1D,wCAAwC;gBACxC,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;SACxD;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,aAAa,CAAC,KAAkB,EAAE,IAAc;QAC9C,QAAQ,IAAI,EAAE;YACZ,KAAK,OAAO;gBACV,OAAO,KAAK,KAAK,IAAI,CAAC;YACxB,KAAK,MAAM;gBACT,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC/C,KAAK,MAAM,CAAC;YACZ,KAAK,KAAK;gBACR,OAAO,IAAI,CAAC,KAAK,CAAC,KAAM,CAAC,CAAC;SAC7B;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CAEF,CAAC;AAMF;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAe,CAAC,KAAc,EAAE,GAAY,EAAW,EAAE;IAC5E,2DAA2D;IAC3D,OAAO,GAAG,KAAK,KAAK,IAAI,CAAC,GAAG,KAAK,GAAG,IAAI,KAAK,KAAK,KAAK,CAAC,CAAC;AAC3D,CAAC,CAAC;AAEF,MAAM,0BAA0B,GAAwB;IACtD,SAAS,EAAE,IAAI;IACf,IAAI,EAAE,MAAM;IACZ,SAAS,EAAE,gBAAgB;IAC3B,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,QAAQ;CACrB,CAAC;AAEF,MAAM,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AAE/C,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,sBAAsB,GAAG,CAAC,IAAI,CAAC,CAAC;AACtC,MAAM,gCAAgC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChD,MAAM,+BAA+B,GAAG,CAAC,IAAI,CAAC,CAAC;AAC/C,MAAM,mBAAmB,GAAG,CAAC,IAAI,CAAC,CAAC;AAKnC;;;;GAIG;AACH,MAAM,OAAgB,eAAgB,SAAQ,WAAW;IA2OvD;QACE,KAAK,EAAE,CAAC;QAlBF,iBAAY,GAAgB,CAAC,CAAC;QAC9B,wBAAmB,GAA6B,SAAS,CAAC;QAC1D,mBAAc,GAAqB,gBAAgB,CAAC;QACpD,0BAAqB,GAA2B,SAAS,CAAC;QAElE;;;WAGG;QACK,uBAAkB,GAAmB,IAAI,GAAG,EAAE,CAAC;QAEvD;;WAEG;QACK,0BAAqB,GACb,SAAS,CAAC;QAIxB,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC;IA/MD;;;OAGG;IACH,MAAM,KAAK,kBAAkB;QAC3B,yDAAyD;QACzD,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,2EAA2E;QAC3E,mBAAmB;QACnB,IAAI,CAAC,gBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACtC,MAAM,IAAI,GAAG,IAAI,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAClD,IAAI,IAAI,KAAK,SAAS,EAAE;gBACtB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBAC1C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aACvB;QACH,CAAC,CAAC,CAAC;QACH,OAAO,UAAU,CAAC;IACpB,CAAC;IAED;;;;OAIG;IACH,kBAAkB;IACV,MAAM,CAAC,sBAAsB;QACnC,oDAAoD;QACpD,IAAI,CAAC,IAAI,CAAC,cAAc,CAChB,yBAAyB,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC,EAAE;YAC5D,IAAI,CAAC,gBAAgB,GAAG,IAAI,GAAG,EAAE,CAAC;YAClC,iEAAiE;YACjE,MAAM,eAAe,GACjB,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,gBAAgB,CAAC;YACjD,IAAI,eAAe,KAAK,SAAS,EAAE;gBACjC,eAAe,CAAC,OAAO,CACnB,CAAC,CAAsB,EAAE,CAAc,EAAE,EAAE,CACvC,IAAI,CAAC,gBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aAC3C;SACF;IACH,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,cAAc,CACjB,IAAiB,EACjB,UAA+B,0BAA0B;QAC3D,oEAAoE;QACpE,qEAAqE;QACrE,YAAY;QACZ,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAC9B,IAAI,CAAC,gBAAiB,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC1C,sEAAsE;QACtE,2EAA2E;QAC3E,mEAAmE;QACnE,yEAAyE;QACzE,qBAAqB;QACrB,IAAI,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE;YAC7D,OAAO;SACR;QACD,MAAM,GAAG,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9D,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE;YAC1C,qDAAqD;YACrD,GAAG;gBACD,qDAAqD;gBACrD,OAAQ,IAAY,CAAC,GAAG,CAAC,CAAC;YAC5B,CAAC;YACD,GAAG,CAAwB,KAAc;gBACvC,qDAAqD;gBACrD,MAAM,QAAQ,GAAI,IAAY,CAAC,IAAI,CAAC,CAAC;gBACrC,qDAAqD;gBACpD,IAAY,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAC3B,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACrC,CAAC;YACD,YAAY,EAAE,IAAI;YAClB,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACO,MAAM,CAAC,QAAQ;QACvB,IAAI,IAAI,CAAC,cAAc,CAAC,yBAAyB,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YACjE,IAAI,CAAC,SAAS,EAAE;YAClB,OAAO;SACR;QACD,4BAA4B;QAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,OAAO,SAAS,CAAC,QAAQ,KAAK,UAAU,EAAE;YAC5C,SAAS,CAAC,QAAQ,EAAE,CAAC;SACtB;QACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAC9B,iDAAiD;QACjD,IAAI,CAAC,uBAAuB,GAAG,IAAI,GAAG,EAAE,CAAC;QACzC,sBAAsB;QACtB,sEAAsE;QACtE,qEAAqE;QACrE,2CAA2C;QAC3C,IAAI,IAAI,CAAC,cAAc,CAAC,yBAAyB,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,EAAE;YACtE,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC;YAC9B,6DAA6D;YAC7D,MAAM,QAAQ,GAAG;gBACf,GAAG,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC;gBACpC,GAAG,CAAC,OAAO,MAAM,CAAC,qBAAqB,KAAK,UAAU,CAAC,CAAC,CAAC;oBACrD,MAAM,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC;oBACrC,EAAE;aACP,CAAC;YACF,iDAAiD;YACjD,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE;gBACxB,uEAAuE;gBACvE,cAAc;gBACd,qDAAqD;gBACrD,IAAI,CAAC,cAAc,CAAC,CAAC,EAAG,KAAa,CAAC,CAAC,CAAC,CAAC,CAAC;aAC3C;SACF;IACH,CAAC;IAED;;;OAGG;IACK,MAAM,CAAC,yBAAyB,CACpC,IAAiB,EAAE,OAA4B;QACjD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACpC,OAAO,SAAS,KAAK,KAAK,CAAC,CAAC;YACxB,SAAS,CAAC,CAAC;YACX,CAAC,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC;gBAC3B,SAAS,CAAC,CAAC;gBACX,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IACxE,CAAC;IAED;;;;;OAKG;IACK,MAAM,CAAC,gBAAgB,CAC3B,KAAc,EAAE,GAAY,EAAE,aAAyB,QAAQ;QACjE,OAAO,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAChC,CAAC;IAED;;;;;OAKG;IACK,MAAM,CAAC,2BAA2B,CACtC,KAAkB,EAAE,OAA4B;QAClD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC1B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,CAAC;QACxD,MAAM,aAAa,GACf,CAAC,OAAO,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QAC5E,OAAO,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAC5D,CAAC;IAED;;;;;;;OAOG;IACK,MAAM,CAAC,yBAAyB,CACpC,KAAc,EAAE,OAA4B;QAC9C,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,EAAE;YACjC,OAAO;SACR;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC1B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACpC,MAAM,WAAW,GACb,SAAS,IAAK,SAAuC,CAAC,WAAW;YACjE,gBAAgB,CAAC,WAAW,CAAC;QACjC,OAAO,WAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IAwBD;;;OAGG;IACO,UAAU;QAClB,IAAI,CAAC,uBAAuB,EAAE,CAAC;IACjC,CAAC;IAED;;;;;;;;;;;OAWG;IACK,uBAAuB;QAC7B,2EAA2E;QAC3E,mBAAmB;QAClB,IAAI,CAAC,WAAsC;aACvC,gBAAiB,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;YACnC,IAAI,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE;gBAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAe,CAAC,CAAC;gBACpC,OAAO,IAAI,CAAC,CAAe,CAAC,CAAC;gBAC7B,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE;oBAC7B,IAAI,CAAC,mBAAmB,GAAG,IAAI,GAAG,EAAE,CAAC;iBACtC;gBACD,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;aACxC;QACH,CAAC,CAAC,CAAC;IACT,CAAC;IAED;;OAEG;IACK,wBAAwB;QAC9B,2EAA2E;QAC3E,mBAAmB;QACnB,kCAAkC;QAClC,IAAI,CAAC,mBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAE,IAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAClE,IAAI,CAAC,mBAAmB,GAAG,SAAS,CAAC;IACvC,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,GAAG,mBAAmB,CAAC;QAC5D,uEAAuE;QACvE,2EAA2E;QAC3E,kEAAkE;QAClE,iBAAiB;QACjB,IAAI,IAAI,CAAC,qBAAqB,EAAE;YAC9B,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC7B,IAAI,CAAC,qBAAqB,GAAG,SAAS,CAAC;SACxC;aAAM;YACL,IAAI,CAAC,aAAa,EAAE,CAAC;SACtB;IACH,CAAC;IAED;;;;OAIG;IACH,oBAAoB;IACpB,CAAC;IAED;;OAEG;IACH,wBAAwB,CAAC,IAAY,EAAE,GAAgB,EAAE,KAAkB;QACzE,IAAI,GAAG,KAAK,KAAK,EAAE;YACjB,IAAI,CAAC,oBAAoB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;SACxC;IACH,CAAC;IAEO,oBAAoB,CACxB,IAAiB,EAAE,KAAc,EACjC,UAA+B,0BAA0B;QAC3D,MAAM,IAAI,GAAI,IAAI,CAAC,WAAsC,CAAC;QAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,yBAAyB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3D,IAAI,IAAI,KAAK,SAAS,EAAE;YACtB,MAAM,SAAS,GAAG,IAAI,CAAC,yBAAyB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACjE,oDAAoD;YACpD,IAAI,SAAS,KAAK,SAAS,EAAE;gBAC3B,OAAO;aACR;YACD,oDAAoD;YACpD,mEAAmE;YACnE,wEAAwE;YACxE,wEAAwE;YACxE,iEAAiE;YACjE,qEAAqE;YACrE,+BAA+B;YAC/B,wBAAwB;YACxB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,GAAG,gCAAgC,CAAC;YACzE,IAAI,SAAS,IAAI,IAAI,EAAE;gBACrB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;aAC5B;iBAAM;gBACL,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,SAAmB,CAAC,CAAC;aAC9C;YACD,4BAA4B;YAC5B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,GAAG,CAAC,gCAAgC,CAAC;SAC3E;IACH,CAAC;IAEO,oBAAoB,CAAC,IAAY,EAAE,KAAkB;QAC3D,qEAAqE;QACrE,mCAAmC;QACnC,IAAI,IAAI,CAAC,YAAY,GAAG,gCAAgC,EAAE;YACxD,OAAO;SACR;QACD,MAAM,IAAI,GAAI,IAAI,CAAC,WAAsC,CAAC;QAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,QAAQ,KAAK,SAAS,EAAE;YAC1B,MAAM,OAAO,GACT,IAAI,CAAC,gBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,0BAA0B,CAAC;YACvE,wBAAwB;YACxB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,GAAG,+BAA+B,CAAC;YACxE,IAAI,CAAC,QAAsB,CAAC;gBACxB,kCAAkC;gBAClC,IAAI,CAAC,2BAA2B,CAAC,KAAK,EAAE,OAAO,CAAQ,CAAC;YAC5D,4BAA4B;YAC5B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,GAAG,CAAC,+BAA+B,CAAC;SAC1E;IACH,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,IAAkB,EAAE,QAAkB;QAClD,IAAI,mBAAmB,GAAG,IAAI,CAAC;QAC/B,4DAA4D;QAC5D,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;YAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,WAAqC,CAAC;YACxD,MAAM,OAAO,GACT,IAAI,CAAC,gBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,0BAA0B,CAAC;YACnE,IAAI,IAAI,CAAC,gBAAgB,CACjB,IAAI,CAAC,IAAkB,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE;gBAC/D,iCAAiC;gBACjC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;gBAC5C,mCAAmC;gBACnC,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;oBACxB,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,+BAA+B,CAAC,EAAE;oBAC1D,IAAI,IAAI,CAAC,qBAAqB,KAAK,SAAS,EAAE;wBAC5C,IAAI,CAAC,qBAAqB,GAAG,IAAI,GAAG,EAAE,CAAC;qBACxC;oBACD,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;iBAC/C;gBACD,sEAAsE;aACvE;iBAAM;gBACL,mBAAmB,GAAG,KAAK,CAAC;aAC7B;SACF;QACD,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,mBAAmB,EAAE;YACpD,IAAI,CAAC,cAAc,EAAE,CAAC;SACvB;QACD,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,cAAc;QAC1B,yBAAyB;QACzB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,GAAG,sBAAsB,CAAC;QAC/D,IAAI,OAA6B,CAAC;QAClC,MAAM,qBAAqB,GAAG,IAAI,CAAC,cAAc,CAAC;QAClD,IAAI,CAAC,cAAc,GAAG,IAAI,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,GAAG,CAAC,CAAC;QAC1D,2DAA2D;QAC3D,+DAA+D;QAC/D,MAAM,qBAAqB,CAAC;QAC5B,uDAAuD;QACvD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE;YACvB,MAAM,IAAI,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,qBAAqB,GAAG,GAAG,CAAC,CAAC;SAC9D;QACD,4EAA4E;QAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACpC,yEAAyE;QACzE,MAAM;QACN,IAAI,MAAM,IAAI,IAAI;YACd,OAAQ,MAA+B,CAAC,IAAI,KAAK,UAAU,EAAE;YAC/D,MAAM,MAAM,CAAC;SACd;QACD,OAAQ,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACtC,CAAC;IAED,IAAY,aAAa;QACvB,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,mBAAmB,CAAC,CAAC;IACnD,CAAC;IAED,IAAY,mBAAmB;QAC7B,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,sBAAsB,CAAC,CAAC;IACtD,CAAC;IAED,IAAc,UAAU;QACtB,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,iBAAiB,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;;;;;;OAYG;IACO,aAAa;QACrB,iDAAiD;QACjD,IAAI,IAAI,CAAC,mBAAmB,EAAE;YAC5B,IAAI,CAAC,wBAAwB,EAAE,CAAC;SACjC;QACD,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,kBAAkB,CAAC,EAAE;YAC9C,MAAM,iBAAiB,GAAG,IAAI,CAAC,kBAAkB,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;YAC/B,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,iBAAiB,CAAC,EAAE;gBAC5C,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,GAAG,iBAAiB,CAAC;gBAC1D,IAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;aACtC;YACD,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;SACjC;aAAM;YACL,IAAI,CAAC,YAAY,EAAE,CAAC;SACrB;IACH,CAAC;IAEO,YAAY;QAClB,IAAI,CAAC,kBAAkB,GAAG,IAAI,GAAG,EAAE,CAAC;QACpC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,GAAG,CAAC,sBAAsB,CAAC;IAClE,CAAC;IAED;;;;;;;;;;;OAWG;IACH,IAAI,cAAc;QAChB,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED;;;;;;OAMG;IACO,YAAY,CAAC,kBAAkC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;OAOG;IACO,MAAM,CAAC,kBAAkC;QACjD,IAAI,IAAI,CAAC,qBAAqB,KAAK,SAAS;YACxC,IAAI,CAAC,qBAAqB,CAAC,IAAI,GAAG,CAAC,EAAE;YACvC,qEAAqE;YACrE,yBAAyB;YACzB,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAC9B,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAe,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACtE,IAAI,CAAC,qBAAqB,GAAG,SAAS,CAAC;SACxC;IACH,CAAC;IAED;;;;;;;;OAQG;IACO,OAAO,CAAC,kBAAkC;IACpD,CAAC;IAED;;;;;;;;OAQG;IACO,YAAY,CAAC,kBAAkC;IACzD,CAAC;;AA9hBD;;GAEG;AACc,yBAAS,GAAG,IAAI,CAAC","sourcesContent":["/**\n * @license\n * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.\n * This code may only be used under the BSD style license found at\n * http://polymer.github.io/LICENSE.txt\n * The complete set of authors may be found at\n * http://polymer.github.io/AUTHORS.txt\n * The complete set of contributors may be found at\n * http://polymer.github.io/CONTRIBUTORS.txt\n * Code distributed by Google as part of the polymer project is also\n * subject to an additional IP rights grant found at\n * http://polymer.github.io/PATENTS.txt\n */\n\n/**\n * When using Closure Compiler, JSCompiler_renameProperty(property, object) is\n * replaced at compile time by the munged name for object[property]. We cannot\n * alias this function, so we have to use a small shim that has the same\n * behavior when not compiling.\n */\nwindow.JSCompiler_renameProperty =\n

(prop: P, _obj: unknown): P => prop;\n\ndeclare global {\n var JSCompiler_renameProperty:

(\n prop: P, _obj: unknown) => P;\n\n interface Window {\n JSCompiler_renameProperty: typeof JSCompiler_renameProperty;\n }\n}\n\n/**\n * Converts property values to and from attribute values.\n */\nexport interface ComplexAttributeConverter {\n /**\n * Function called to convert an attribute value to a property\n * value.\n */\n fromAttribute?(value: string|null, type?: TypeHint): Type;\n\n /**\n * Function called to convert a property value to an attribute\n * value.\n *\n * It returns unknown instead of string, to be compatible with\n * https://github.com/WICG/trusted-types (and similar efforts).\n */\n toAttribute?(value: Type, type?: TypeHint): unknown;\n}\n\ntype AttributeConverter =\n ComplexAttributeConverter|((value: string, type?: TypeHint) => Type);\n\n/**\n * Defines options for a property accessor.\n */\nexport interface PropertyDeclaration {\n /**\n * Indicates how and whether the property becomes an observed attribute.\n * If the value is `false`, the property is not added to `observedAttributes`.\n * If true or absent, the lowercased property name is observed (e.g. `fooBar`\n * becomes `foobar`). If a string, the string value is observed (e.g\n * `attribute: 'foo-bar'`).\n */\n readonly attribute?: boolean|string;\n\n /**\n * Indicates the type of the property. This is used only as a hint for the\n * `converter` to determine how to convert the attribute\n * to/from a property.\n */\n readonly type?: TypeHint;\n\n /**\n * Indicates how to convert the attribute to/from a property. If this value\n * is a function, it is used to convert the attribute value a the property\n * value. If it's an object, it can have keys for `fromAttribute` and\n * `toAttribute`. If no `toAttribute` function is provided and\n * `reflect` is set to `true`, the property value is set directly to the\n * attribute. A default `converter` is used if none is provided; it supports\n * `Boolean`, `String`, `Number`, `Object`, and `Array`. Note,\n * when a property changes and the converter is used to update the attribute,\n * the property is never updated again as a result of the attribute changing,\n * and vice versa.\n */\n readonly converter?: AttributeConverter;\n\n /**\n * Indicates if the property should reflect to an attribute.\n * If `true`, when the property is set, the attribute is set using the\n * attribute name determined according to the rules for the `attribute`\n * property option and the value of the property converted using the rules\n * from the `converter` property option.\n */\n readonly reflect?: boolean;\n\n /**\n * A function that indicates if a property should be considered changed when\n * it is set. The function should take the `newValue` and `oldValue` and\n * return `true` if an update should be requested.\n */\n hasChanged?(value: Type, oldValue: Type): boolean;\n\n /**\n * Indicates whether an accessor will be created for this property. By\n * default, an accessor will be generated for this property that requests an\n * update when set. If this flag is `true`, no accessor will be created, and\n * it will be the user's responsibility to call\n * `this.requestUpdate(propertyName, oldValue)` to request an update when\n * the property changes.\n */\n readonly noAccessor?: boolean;\n}\n\n/**\n * Map of properties to PropertyDeclaration options. For each property an\n * accessor is made, and the property is processed according to the\n * PropertyDeclaration options.\n */\nexport interface PropertyDeclarations {\n readonly [key: string]: PropertyDeclaration;\n}\n\ntype PropertyDeclarationMap = Map;\n\ntype AttributeMap = Map;\n\nexport type PropertyValues = Map;\n\nexport const defaultConverter: ComplexAttributeConverter = {\n\n toAttribute(value: unknown, type?: unknown): unknown {\n switch (type) {\n case Boolean:\n return value ? '' : null;\n case Object:\n case Array:\n // if the value is `null` or `undefined` pass this through\n // to allow removing/no change behavior.\n return value == null ? value : JSON.stringify(value);\n }\n return value;\n },\n\n fromAttribute(value: string|null, type?: unknown) {\n switch (type) {\n case Boolean:\n return value !== null;\n case Number:\n return value === null ? null : Number(value);\n case Object:\n case Array:\n return JSON.parse(value!);\n }\n return value;\n }\n\n};\n\nexport interface HasChanged {\n (value: unknown, old: unknown): boolean;\n}\n\n/**\n * Change function that returns true if `value` is different from `oldValue`.\n * This method is used as the default for a property's `hasChanged` function.\n */\nexport const notEqual: HasChanged = (value: unknown, old: unknown): boolean => {\n // This ensures (old==NaN, value==NaN) always returns false\n return old !== value && (old === old || value === value);\n};\n\nconst defaultPropertyDeclaration: PropertyDeclaration = {\n attribute: true,\n type: String,\n converter: defaultConverter,\n reflect: false,\n hasChanged: notEqual\n};\n\nconst microtaskPromise = Promise.resolve(true);\n\nconst STATE_HAS_UPDATED = 1;\nconst STATE_UPDATE_REQUESTED = 1 << 2;\nconst STATE_IS_REFLECTING_TO_ATTRIBUTE = 1 << 3;\nconst STATE_IS_REFLECTING_TO_PROPERTY = 1 << 4;\nconst STATE_HAS_CONNECTED = 1 << 5;\ntype UpdateState = typeof STATE_HAS_UPDATED|typeof STATE_UPDATE_REQUESTED|\n typeof STATE_IS_REFLECTING_TO_ATTRIBUTE|\n typeof STATE_IS_REFLECTING_TO_PROPERTY|typeof STATE_HAS_CONNECTED;\n\n/**\n * Base element class which manages element properties and attributes. When\n * properties change, the `update` method is asynchronously called. This method\n * should be supplied by subclassers to render updates as desired.\n */\nexport abstract class UpdatingElement extends HTMLElement {\n /*\n * Due to closure compiler ES6 compilation bugs, @nocollapse is required on\n * all static methods and properties with initializers. Reference:\n * - https://github.com/google/closure-compiler/issues/1776\n */\n\n /**\n * Maps attribute names to properties; for example `foobar` attribute to\n * `fooBar` property. Created lazily on user subclasses when finalizing the\n * class.\n */\n private static _attributeToPropertyMap: AttributeMap;\n\n /**\n * Marks class as having finished creating properties.\n */\n protected static finalized = true;\n\n /**\n * Memoized list of all class properties, including any superclass properties.\n * Created lazily on user subclasses when finalizing the class.\n */\n private static _classProperties?: PropertyDeclarationMap;\n\n /**\n * User-supplied object that maps property names to `PropertyDeclaration`\n * objects containing options for configuring the property.\n */\n static properties: PropertyDeclarations;\n\n /**\n * Returns a list of attributes corresponding to the registered properties.\n * @nocollapse\n */\n static get observedAttributes() {\n // note: piggy backing on this to ensure we're finalized.\n this.finalize();\n const attributes: string[] = [];\n // Use forEach so this works even if for/of loops are compiled to for loops\n // expecting arrays\n this._classProperties!.forEach((v, p) => {\n const attr = this._attributeNameForProperty(p, v);\n if (attr !== undefined) {\n this._attributeToPropertyMap.set(attr, p);\n attributes.push(attr);\n }\n });\n return attributes;\n }\n\n /**\n * Ensures the private `_classProperties` property metadata is created.\n * In addition to `finalize` this is also called in `createProperty` to\n * ensure the `@property` decorator can add property metadata.\n */\n /** @nocollapse */\n private static _ensureClassProperties() {\n // ensure private storage for property declarations.\n if (!this.hasOwnProperty(\n JSCompiler_renameProperty('_classProperties', this))) {\n this._classProperties = new Map();\n // NOTE: Workaround IE11 not supporting Map constructor argument.\n const superProperties: PropertyDeclarationMap =\n Object.getPrototypeOf(this)._classProperties;\n if (superProperties !== undefined) {\n superProperties.forEach(\n (v: PropertyDeclaration, k: PropertyKey) =>\n this._classProperties!.set(k, v));\n }\n }\n }\n\n /**\n * Creates a property accessor on the element prototype if one does not exist.\n * The property setter calls the property's `hasChanged` property option\n * or uses a strict identity check to determine whether or not to request\n * an update.\n * @nocollapse\n */\n static createProperty(\n name: PropertyKey,\n options: PropertyDeclaration = defaultPropertyDeclaration) {\n // Note, since this can be called by the `@property` decorator which\n // is called before `finalize`, we ensure storage exists for property\n // metadata.\n this._ensureClassProperties();\n this._classProperties!.set(name, options);\n // Do not generate an accessor if the prototype already has one, since\n // it would be lost otherwise and that would never be the user's intention;\n // Instead, we expect users to call `requestUpdate` themselves from\n // user-defined accessors. Note that if the super has an accessor we will\n // still overwrite it\n if (options.noAccessor || this.prototype.hasOwnProperty(name)) {\n return;\n }\n const key = typeof name === 'symbol' ? Symbol() : `__${name}`;\n Object.defineProperty(this.prototype, name, {\n // tslint:disable-next-line:no-any no symbol in index\n get(): any {\n // tslint:disable-next-line:no-any no symbol in index\n return (this as any)[key];\n },\n set(this: UpdatingElement, value: unknown) {\n // tslint:disable-next-line:no-any no symbol in index\n const oldValue = (this as any)[name];\n // tslint:disable-next-line:no-any no symbol in index\n (this as any)[key] = value;\n this.requestUpdate(name, oldValue);\n },\n configurable: true,\n enumerable: true\n });\n }\n\n /**\n * Creates property accessors for registered properties and ensures\n * any superclasses are also finalized.\n * @nocollapse\n */\n protected static finalize() {\n if (this.hasOwnProperty(JSCompiler_renameProperty('finalized', this)) &&\n this.finalized) {\n return;\n }\n // finalize any superclasses\n const superCtor = Object.getPrototypeOf(this);\n if (typeof superCtor.finalize === 'function') {\n superCtor.finalize();\n }\n this.finalized = true;\n this._ensureClassProperties();\n // initialize Map populated in observedAttributes\n this._attributeToPropertyMap = new Map();\n // make any properties\n // Note, only process \"own\" properties since this element will inherit\n // any properties defined on the superClass, and finalization ensures\n // the entire prototype chain is finalized.\n if (this.hasOwnProperty(JSCompiler_renameProperty('properties', this))) {\n const props = this.properties;\n // support symbols in properties (IE11 does not support this)\n const propKeys = [\n ...Object.getOwnPropertyNames(props),\n ...(typeof Object.getOwnPropertySymbols === 'function') ?\n Object.getOwnPropertySymbols(props) :\n []\n ];\n // This for/of is ok because propKeys is an array\n for (const p of propKeys) {\n // note, use of `any` is due to TypeSript lack of support for symbol in\n // index types\n // tslint:disable-next-line:no-any no symbol in index\n this.createProperty(p, (props as any)[p]);\n }\n }\n }\n\n /**\n * Returns the property name for the given attribute `name`.\n * @nocollapse\n */\n private static _attributeNameForProperty(\n name: PropertyKey, options: PropertyDeclaration) {\n const attribute = options.attribute;\n return attribute === false ?\n undefined :\n (typeof attribute === 'string' ?\n attribute :\n (typeof name === 'string' ? name.toLowerCase() : undefined));\n }\n\n /**\n * Returns true if a property should request an update.\n * Called when a property value is set and uses the `hasChanged`\n * option for the property if present or a strict identity check.\n * @nocollapse\n */\n private static _valueHasChanged(\n value: unknown, old: unknown, hasChanged: HasChanged = notEqual) {\n return hasChanged(value, old);\n }\n\n /**\n * Returns the property value for the given attribute value.\n * Called via the `attributeChangedCallback` and uses the property's\n * `converter` or `converter.fromAttribute` property option.\n * @nocollapse\n */\n private static _propertyValueFromAttribute(\n value: string|null, options: PropertyDeclaration) {\n const type = options.type;\n const converter = options.converter || defaultConverter;\n const fromAttribute =\n (typeof converter === 'function' ? converter : converter.fromAttribute);\n return fromAttribute ? fromAttribute(value, type) : value;\n }\n\n /**\n * Returns the attribute value for the given property value. If this\n * returns undefined, the property will *not* be reflected to an attribute.\n * If this returns null, the attribute will be removed, otherwise the\n * attribute will be set to the value.\n * This uses the property's `reflect` and `type.toAttribute` property options.\n * @nocollapse\n */\n private static _propertyValueToAttribute(\n value: unknown, options: PropertyDeclaration) {\n if (options.reflect === undefined) {\n return;\n }\n const type = options.type;\n const converter = options.converter;\n const toAttribute =\n converter && (converter as ComplexAttributeConverter).toAttribute ||\n defaultConverter.toAttribute;\n return toAttribute!(value, type);\n }\n\n private _updateState: UpdateState = 0;\n private _instanceProperties: PropertyValues|undefined = undefined;\n private _updatePromise: Promise = microtaskPromise;\n private _hasConnectedResolver: (() => void)|undefined = undefined;\n\n /**\n * Map with keys for any properties that have changed since the last\n * update cycle with previous values.\n */\n private _changedProperties: PropertyValues = new Map();\n\n /**\n * Map with keys of properties that should be reflected when updated.\n */\n private _reflectingProperties: Map|\n undefined = undefined;\n\n constructor() {\n super();\n this.initialize();\n }\n\n /**\n * Performs element initialization. By default captures any pre-set values for\n * registered properties.\n */\n protected initialize() {\n this._saveInstanceProperties();\n }\n\n /**\n * Fixes any properties set on the instance before upgrade time.\n * Otherwise these would shadow the accessor and break these properties.\n * The properties are stored in a Map which is played back after the\n * constructor runs. Note, on very old versions of Safari (<=9) or Chrome\n * (<=41), properties created for native platform properties like (`id` or\n * `name`) may not have default values set in the element constructor. On\n * these browsers native properties appear on instances and therefore their\n * default value will overwrite any element default (e.g. if the element sets\n * this.id = 'id' in the constructor, the 'id' will become '' since this is\n * the native platform default).\n */\n private _saveInstanceProperties() {\n // Use forEach so this works even if for/of loops are compiled to for loops\n // expecting arrays\n (this.constructor as typeof UpdatingElement)\n ._classProperties!.forEach((_v, p) => {\n if (this.hasOwnProperty(p)) {\n const value = this[p as keyof this];\n delete this[p as keyof this];\n if (!this._instanceProperties) {\n this._instanceProperties = new Map();\n }\n this._instanceProperties.set(p, value);\n }\n });\n }\n\n /**\n * Applies previously saved instance properties.\n */\n private _applyInstanceProperties() {\n // Use forEach so this works even if for/of loops are compiled to for loops\n // expecting arrays\n // tslint:disable-next-line:no-any\n this._instanceProperties!.forEach((v, p) => (this as any)[p] = v);\n this._instanceProperties = undefined;\n }\n\n connectedCallback() {\n this._updateState = this._updateState | STATE_HAS_CONNECTED;\n // Ensure connection triggers an update. Updates cannot complete before\n // connection and if one is pending connection the `_hasConnectionResolver`\n // will exist. If so, resolve it to complete the update, otherwise\n // requestUpdate.\n if (this._hasConnectedResolver) {\n this._hasConnectedResolver();\n this._hasConnectedResolver = undefined;\n } else {\n this.requestUpdate();\n }\n }\n\n /**\n * Allows for `super.disconnectedCallback()` in extensions while\n * reserving the possibility of making non-breaking feature additions\n * when disconnecting at some point in the future.\n */\n disconnectedCallback() {\n }\n\n /**\n * Synchronizes property values when attributes change.\n */\n attributeChangedCallback(name: string, old: string|null, value: string|null) {\n if (old !== value) {\n this._attributeToProperty(name, value);\n }\n }\n\n private _propertyToAttribute(\n name: PropertyKey, value: unknown,\n options: PropertyDeclaration = defaultPropertyDeclaration) {\n const ctor = (this.constructor as typeof UpdatingElement);\n const attr = ctor._attributeNameForProperty(name, options);\n if (attr !== undefined) {\n const attrValue = ctor._propertyValueToAttribute(value, options);\n // an undefined value does not change the attribute.\n if (attrValue === undefined) {\n return;\n }\n // Track if the property is being reflected to avoid\n // setting the property again via `attributeChangedCallback`. Note:\n // 1. this takes advantage of the fact that the callback is synchronous.\n // 2. will behave incorrectly if multiple attributes are in the reaction\n // stack at time of calling. However, since we process attributes\n // in `update` this should not be possible (or an extreme corner case\n // that we'd like to discover).\n // mark state reflecting\n this._updateState = this._updateState | STATE_IS_REFLECTING_TO_ATTRIBUTE;\n if (attrValue == null) {\n this.removeAttribute(attr);\n } else {\n this.setAttribute(attr, attrValue as string);\n }\n // mark state not reflecting\n this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_ATTRIBUTE;\n }\n }\n\n private _attributeToProperty(name: string, value: string|null) {\n // Use tracking info to avoid deserializing attribute value if it was\n // just set from a property setter.\n if (this._updateState & STATE_IS_REFLECTING_TO_ATTRIBUTE) {\n return;\n }\n const ctor = (this.constructor as typeof UpdatingElement);\n const propName = ctor._attributeToPropertyMap.get(name);\n if (propName !== undefined) {\n const options =\n ctor._classProperties!.get(propName) || defaultPropertyDeclaration;\n // mark state reflecting\n this._updateState = this._updateState | STATE_IS_REFLECTING_TO_PROPERTY;\n this[propName as keyof this] =\n // tslint:disable-next-line:no-any\n ctor._propertyValueFromAttribute(value, options) as any;\n // mark state not reflecting\n this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_PROPERTY;\n }\n }\n\n /**\n * Requests an update which is processed asynchronously. This should\n * be called when an element should update based on some state not triggered\n * by setting a property. In this case, pass no arguments. It should also be\n * called when manually implementing a property setter. In this case, pass the\n * property `name` and `oldValue` to ensure that any configured property\n * options are honored. Returns the `updateComplete` Promise which is resolved\n * when the update completes.\n *\n * @param name {PropertyKey} (optional) name of requesting property\n * @param oldValue {any} (optional) old value of requesting property\n * @returns {Promise} A Promise that is resolved when the update completes.\n */\n requestUpdate(name?: PropertyKey, oldValue?: unknown) {\n let shouldRequestUpdate = true;\n // if we have a property key, perform property update steps.\n if (name !== undefined && !this._changedProperties.has(name)) {\n const ctor = this.constructor as typeof UpdatingElement;\n const options =\n ctor._classProperties!.get(name) || defaultPropertyDeclaration;\n if (ctor._valueHasChanged(\n this[name as keyof this], oldValue, options.hasChanged)) {\n // track old value when changing.\n this._changedProperties.set(name, oldValue);\n // add to reflecting properties set\n if (options.reflect === true &&\n !(this._updateState & STATE_IS_REFLECTING_TO_PROPERTY)) {\n if (this._reflectingProperties === undefined) {\n this._reflectingProperties = new Map();\n }\n this._reflectingProperties.set(name, options);\n }\n // abort the request if the property should not be considered changed.\n } else {\n shouldRequestUpdate = false;\n }\n }\n if (!this._hasRequestedUpdate && shouldRequestUpdate) {\n this._enqueueUpdate();\n }\n return this.updateComplete;\n }\n\n /**\n * Sets up the element to asynchronously update.\n */\n private async _enqueueUpdate() {\n // Mark state updating...\n this._updateState = this._updateState | STATE_UPDATE_REQUESTED;\n let resolve: (r: boolean) => void;\n const previousUpdatePromise = this._updatePromise;\n this._updatePromise = new Promise((res) => resolve = res);\n // Ensure any previous update has resolved before updating.\n // This `await` also ensures that property changes are batched.\n await previousUpdatePromise;\n // Make sure the element has connected before updating.\n if (!this._hasConnected) {\n await new Promise((res) => this._hasConnectedResolver = res);\n }\n // Allow `performUpdate` to be asynchronous to enable scheduling of updates.\n const result = this.performUpdate();\n // Note, this is to avoid delaying an additional microtask unless we need\n // to.\n if (result != null &&\n typeof (result as PromiseLike).then === 'function') {\n await result;\n }\n resolve!(!this._hasRequestedUpdate);\n }\n\n private get _hasConnected() {\n return (this._updateState & STATE_HAS_CONNECTED);\n }\n\n private get _hasRequestedUpdate() {\n return (this._updateState & STATE_UPDATE_REQUESTED);\n }\n\n protected get hasUpdated() {\n return (this._updateState & STATE_HAS_UPDATED);\n }\n\n /**\n * Performs an element update.\n *\n * You can override this method to change the timing of updates. For instance,\n * to schedule updates to occur just before the next frame:\n *\n * ```\n * protected async performUpdate(): Promise {\n * await new Promise((resolve) => requestAnimationFrame(() => resolve()));\n * super.performUpdate();\n * }\n * ```\n */\n protected performUpdate(): void|Promise {\n // Mixin instance properties once, if they exist.\n if (this._instanceProperties) {\n this._applyInstanceProperties();\n }\n if (this.shouldUpdate(this._changedProperties)) {\n const changedProperties = this._changedProperties;\n this.update(changedProperties);\n this._markUpdated();\n if (!(this._updateState & STATE_HAS_UPDATED)) {\n this._updateState = this._updateState | STATE_HAS_UPDATED;\n this.firstUpdated(changedProperties);\n }\n this.updated(changedProperties);\n } else {\n this._markUpdated();\n }\n }\n\n private _markUpdated() {\n this._changedProperties = new Map();\n this._updateState = this._updateState & ~STATE_UPDATE_REQUESTED;\n }\n\n /**\n * Returns a Promise that resolves when the element has completed updating.\n * The Promise value is a boolean that is `true` if the element completed the\n * update without triggering another update. The Promise result is `false` if\n * a property was set inside `updated()`. This getter can be implemented to\n * await additional state. For example, it is sometimes useful to await a\n * rendered element before fulfilling this Promise. To do this, first await\n * `super.updateComplete` then any subsequent state.\n *\n * @returns {Promise} The Promise returns a boolean that indicates if the\n * update resolved without triggering another update.\n */\n get updateComplete() {\n return this._updatePromise;\n }\n\n /**\n * Controls whether or not `update` should be called when the element requests\n * an update. By default, this method always returns `true`, but this can be\n * customized to control when to update.\n *\n * * @param _changedProperties Map of changed properties with old values\n */\n protected shouldUpdate(_changedProperties: PropertyValues): boolean {\n return true;\n }\n\n /**\n * Updates the element. This method reflects property values to attributes.\n * It can be overridden to render and keep updated element DOM.\n * Setting properties inside this method will *not* trigger\n * another update.\n *\n * * @param _changedProperties Map of changed properties with old values\n */\n protected update(_changedProperties: PropertyValues) {\n if (this._reflectingProperties !== undefined &&\n this._reflectingProperties.size > 0) {\n // Use forEach so this works even if for/of loops are compiled to for\n // loops expecting arrays\n this._reflectingProperties.forEach(\n (v, k) => this._propertyToAttribute(k, this[k as keyof this], v));\n this._reflectingProperties = undefined;\n }\n }\n\n /**\n * Invoked whenever the element is updated. Implement to perform\n * post-updating tasks via DOM APIs, for example, focusing an element.\n *\n * Setting properties inside this method will trigger the element to update\n * again after this update cycle completes.\n *\n * * @param _changedProperties Map of changed properties with old values\n */\n protected updated(_changedProperties: PropertyValues) {\n }\n\n /**\n * Invoked when the element is first updated. Implement to perform one time\n * work on the element after update.\n *\n * Setting properties inside this method will trigger the element to update\n * again after this update cycle completes.\n *\n * * @param _changedProperties Map of changed properties with old values\n */\n protected firstUpdated(_changedProperties: PropertyValues) {\n }\n}\n"]} \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lit-element.js b/app/userland/app-stdlib/vendor/lit-element/lit-element.js new file mode 100644 index 0000000000..3c11f023e1 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lit-element.js @@ -0,0 +1,199 @@ +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ +import { TemplateResult } from './lit-html/lit-html.js'; +import { render } from './lit-html/lib/shady-render.js'; +import { UpdatingElement } from './lib/updating-element.js'; +export * from './lib/updating-element.js'; +export * from './lib/decorators.js'; +export { html, svg, TemplateResult, SVGTemplateResult } from './lit-html/lit-html.js'; +import { supportsAdoptingStyleSheets } from './lib/css-tag.js'; +export * from './lib/css-tag.js'; +// IMPORTANT: do not change the property name or the assignment expression. +// This line will be used in regexes to search for LitElement usage. +// TODO(justinfagnani): inject version number at build time +(window['litElementVersions'] || (window['litElementVersions'] = [])) + .push('2.0.1'); +/** + * Minimal implementation of Array.prototype.flat + * @param arr the array to flatten + * @param result the accumlated result + */ +function arrayFlat(styles, result = []) { + for (let i = 0, length = styles.length; i < length; i++) { + const value = styles[i]; + if (Array.isArray(value)) { + arrayFlat(value, result); + } + else { + result.push(value); + } + } + return result; +} +/** Deeply flattens styles array. Uses native flat if available. */ +const flattenStyles = (styles) => styles.flat ? styles.flat(Infinity) : arrayFlat(styles); +export class LitElement extends UpdatingElement { + /** @nocollapse */ + static finalize() { + super.finalize(); + // Prepare styling that is stamped at first render time. Styling + // is built from user provided `styles` or is inherited from the superclass. + this._styles = + this.hasOwnProperty(JSCompiler_renameProperty('styles', this)) ? + this._getUniqueStyles() : + this._styles || []; + } + /** @nocollapse */ + static _getUniqueStyles() { + // Take care not to call `this.styles` multiple times since this generates + // new CSSResults each time. + // TODO(sorvell): Since we do not cache CSSResults by input, any + // shared styles will generate new stylesheet objects, which is wasteful. + // This should be addressed when a browser ships constructable + // stylesheets. + const userStyles = this.styles; + const styles = []; + if (Array.isArray(userStyles)) { + const flatStyles = flattenStyles(userStyles); + // As a performance optimization to avoid duplicated styling that can + // occur especially when composing via subclassing, de-duplicate styles + // preserving the last item in the list. The last item is kept to + // try to preserve cascade order with the assumption that it's most + // important that last added styles override previous styles. + const styleSet = flatStyles.reduceRight((set, s) => { + set.add(s); + // on IE set.add does not return the set. + return set; + }, new Set()); + // Array.from does not work on Set in IE + styleSet.forEach((v) => styles.unshift(v)); + } + else if (userStyles) { + styles.push(userStyles); + } + return styles; + } + /** + * Performs element initialization. By default this calls `createRenderRoot` + * to create the element `renderRoot` node and captures any pre-set values for + * registered properties. + */ + initialize() { + super.initialize(); + this.renderRoot = this.createRenderRoot(); + // Note, if renderRoot is not a shadowRoot, styles would/could apply to the + // element's getRootNode(). While this could be done, we're choosing not to + // support this now since it would require different logic around de-duping. + if (window.ShadowRoot && this.renderRoot instanceof window.ShadowRoot) { + this.adoptStyles(); + } + } + /** + * Returns the node into which the element should render and by default + * creates and returns an open shadowRoot. Implement to customize where the + * element's DOM is rendered. For example, to render into the element's + * childNodes, return `this`. + * @returns {Element|DocumentFragment} Returns a node into which to render. + */ + createRenderRoot() { + return this.attachShadow({ mode: 'open' }); + } + /** + * Applies styling to the element shadowRoot using the `static get styles` + * property. Styling will apply using `shadowRoot.adoptedStyleSheets` where + * available and will fallback otherwise. When Shadow DOM is polyfilled, + * ShadyCSS scopes styles and adds them to the document. When Shadow DOM + * is available but `adoptedStyleSheets` is not, styles are appended to the + * end of the `shadowRoot` to [mimic spec + * behavior](https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets). + */ + adoptStyles() { + const styles = this.constructor._styles; + if (styles.length === 0) { + return; + } + // There are three separate cases here based on Shadow DOM support. + // (1) shadowRoot polyfilled: use ShadyCSS + // (2) shadowRoot.adoptedStyleSheets available: use it. + // (3) shadowRoot.adoptedStyleSheets polyfilled: append styles after + // rendering + if (window.ShadyCSS !== undefined && !window.ShadyCSS.nativeShadow) { + window.ShadyCSS.ScopingShim.prepareAdoptedCssText(styles.map((s) => s.cssText), this.localName); + } + else if (supportsAdoptingStyleSheets) { + this.renderRoot.adoptedStyleSheets = + styles.map((s) => s.styleSheet); + } + else { + // This must be done after rendering so the actual style insertion is done + // in `update`. + this._needsShimAdoptedStyleSheets = true; + } + } + connectedCallback() { + super.connectedCallback(); + // Note, first update/render handles styleElement so we only call this if + // connected after first update. + if (this.hasUpdated && window.ShadyCSS !== undefined) { + window.ShadyCSS.styleElement(this); + } + } + /** + * Updates the element. This method reflects property values to attributes + * and calls `render` to render DOM via lit-html. Setting properties inside + * this method will *not* trigger another update. + * * @param _changedProperties Map of changed properties with old values + */ + update(changedProperties) { + super.update(changedProperties); + const templateResult = this.render(); + if (templateResult instanceof TemplateResult) { + this.constructor + .render(templateResult, this.renderRoot, { scopeName: this.localName, eventContext: this }); + } + // When native Shadow DOM is used but adoptedStyles are not supported, + // insert styling after rendering to ensure adoptedStyles have highest + // priority. + if (this._needsShimAdoptedStyleSheets) { + this._needsShimAdoptedStyleSheets = false; + this.constructor._styles.forEach((s) => { + const style = document.createElement('style'); + style.textContent = s.cssText; + this.renderRoot.appendChild(style); + }); + } + } + /** + * Invoked on each update to perform rendering tasks. This method must return + * a lit-html TemplateResult. Setting properties inside this method will *not* + * trigger the element to update. + */ + render() { + } +} +/** + * Ensure this class is marked as `finalized` as an optimization ensuring + * it will not needlessly try to `finalize`. + */ +LitElement.finalized = true; +/** + * Render method used to render the lit-html TemplateResult to the element's + * DOM. + * @param {TemplateResult} Template to render. + * @param {Element|DocumentFragment} Node into which to render. + * @param {String} Element name. + * @nocollapse + */ +LitElement.render = render; +//# sourceMappingURL=lit-element.js.map \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lit-element.js.map b/app/userland/app-stdlib/vendor/lit-element/lit-element.js.map new file mode 100644 index 0000000000..e24fb86641 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lit-element.js.map @@ -0,0 +1 @@ +{"version":3,"file":"lit-element.js","sourceRoot":"","sources":["src/lit-element.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAC,cAAc,EAAC,MAAM,UAAU,CAAC;AACxC,OAAO,EAAC,MAAM,EAAC,MAAM,2BAA2B,CAAC;AAEjD,OAAO,EAAiB,eAAe,EAAC,MAAM,2BAA2B,CAAC;AAE1E,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAC,IAAI,EAAE,GAAG,EAAE,cAAc,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAC,2BAA2B,EAAY,MAAM,kBAAkB,CAAC;AACxE,cAAc,kBAAkB,CAAC;AAQjC,2EAA2E;AAC3E,oEAAoE;AACpE,2DAA2D;AAC3D,CAAC,MAAM,CAAC,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC,CAAC;KAChE,IAAI,CAAC,OAAO,CAAC,CAAC;AAInB;;;;GAIG;AACH,SAAS,SAAS,CACd,MAAsB,EAAE,SAAsB,EAAE;IAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE;QACvD,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;YACxB,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;SAC1B;aAAM;YACL,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;SACpB;KACF;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,mEAAmE;AACnE,MAAM,aAAa,GAAG,CAAC,MAAsB,EAAe,EAAE,CAC1D,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAE5D,MAAM,OAAO,UAAW,SAAQ,eAAe;IAyB7C,kBAAkB;IACR,MAAM,CAAC,QAAQ;QACvB,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjB,gEAAgE;QAChE,4EAA4E;QAC5E,IAAI,CAAC,OAAO;YACR,IAAI,CAAC,cAAc,CAAC,yBAAyB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;gBAChE,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;gBACzB,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;IACzB,CAAC;IAED,kBAAkB;IACV,MAAM,CAAC,gBAAgB;QAC7B,0EAA0E;QAC1E,4BAA4B;QAC5B,gEAAgE;QAChE,yEAAyE;QACzE,8DAA8D;QAC9D,eAAe;QACf,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;QAC/B,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;YAC7B,MAAM,UAAU,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;YAC7C,qEAAqE;YACrE,uEAAuE;YACvE,iEAAiE;YACjE,mEAAmE;YACnE,6DAA6D;YAC7D,MAAM,QAAQ,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;gBACjD,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACX,yCAAyC;gBACzC,OAAO,GAAG,CAAC;YACb,CAAC,EAAE,IAAI,GAAG,EAAa,CAAC,CAAC;YACzB,wCAAwC;YACxC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;SAC7C;aAAM,IAAI,UAAU,EAAE;YACrB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;SACzB;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAUD;;;;OAIG;IACO,UAAU;QAClB,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1C,2EAA2E;QAC3E,2EAA2E;QAC3E,4EAA4E;QAC5E,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,YAAY,MAAM,CAAC,UAAU,EAAE;YACrE,IAAI,CAAC,WAAW,EAAE,CAAC;SACpB;IACH,CAAC;IAED;;;;;;OAMG;IACO,gBAAgB;QACxB,OAAO,IAAI,CAAC,YAAY,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC,CAAC;IAC3C,CAAC;IAED;;;;;;;;OAQG;IACO,WAAW;QACnB,MAAM,MAAM,GAAI,IAAI,CAAC,WAAiC,CAAC,OAAQ,CAAC;QAChE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE;YACvB,OAAO;SACR;QACD,mEAAmE;QACnE,0CAA0C;QAC1C,uDAAuD;QACvD,oEAAoE;QACpE,YAAY;QACZ,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE;YAClE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,qBAAqB,CAC7C,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;SACnD;aAAM,IAAI,2BAA2B,EAAE;YACrC,IAAI,CAAC,UAAyB,CAAC,kBAAkB;gBAC9C,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAW,CAAC,CAAC;SACtC;aAAM;YACL,0EAA0E;YAC1E,eAAe;YACf,IAAI,CAAC,4BAA4B,GAAG,IAAI,CAAC;SAC1C;IACH,CAAC;IAED,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,yEAAyE;QACzE,gCAAgC;QAChC,IAAI,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE;YACpD,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;SACpC;IACH,CAAC;IAED;;;;;OAKG;IACO,MAAM,CAAC,iBAAiC;QAChD,KAAK,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAChC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,EAAa,CAAC;QAChD,IAAI,cAAc,YAAY,cAAc,EAAE;YAC3C,IAAI,CAAC,WAAiC;iBAClC,MAAM,CACH,cAAc,EACd,IAAI,CAAC,UAAW,EAChB,EAAC,SAAS,EAAE,IAAI,CAAC,SAAU,EAAE,YAAY,EAAE,IAAI,EAAC,CAAC,CAAC;SAC3D;QACD,sEAAsE;QACtE,sEAAsE;QACtE,YAAY;QACZ,IAAI,IAAI,CAAC,4BAA4B,EAAE;YACrC,IAAI,CAAC,4BAA4B,GAAG,KAAK,CAAC;YACzC,IAAI,CAAC,WAAiC,CAAC,OAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;gBAC7D,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;gBAC9C,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC,OAAO,CAAC;gBAC9B,IAAI,CAAC,UAAW,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;SACJ;IACH,CAAC;IAED;;;;OAIG;IACO,MAAM;IAChB,CAAC;;AAhLD;;;GAGG;AACc,oBAAS,GAAG,IAAI,CAAC;AAElC;;;;;;;GAOG;AACI,iBAAM,GAAG,MAAM,CAAC","sourcesContent":["/**\n * @license\n * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.\n * This code may only be used under the BSD style license found at\n * http://polymer.github.io/LICENSE.txt\n * The complete set of authors may be found at\n * http://polymer.github.io/AUTHORS.txt\n * The complete set of contributors may be found at\n * http://polymer.github.io/CONTRIBUTORS.txt\n * Code distributed by Google as part of the polymer project is also\n * subject to an additional IP rights grant found at\n * http://polymer.github.io/PATENTS.txt\n */\nimport {TemplateResult} from 'lit-html';\nimport {render} from 'lit-html/lib/shady-render';\n\nimport {PropertyValues, UpdatingElement} from './lib/updating-element.js';\n\nexport * from './lib/updating-element.js';\nexport * from './lib/decorators.js';\nexport {html, svg, TemplateResult, SVGTemplateResult} from 'lit-html/lit-html';\nimport {supportsAdoptingStyleSheets, CSSResult} from './lib/css-tag.js';\nexport * from './lib/css-tag.js';\n\ndeclare global {\n interface Window {\n litElementVersions: string[];\n }\n}\n\n// IMPORTANT: do not change the property name or the assignment expression.\n// This line will be used in regexes to search for LitElement usage.\n// TODO(justinfagnani): inject version number at build time\n(window['litElementVersions'] || (window['litElementVersions'] = []))\n .push('2.0.1');\n\nexport interface CSSResultArray extends Array {}\n\n/**\n * Minimal implementation of Array.prototype.flat\n * @param arr the array to flatten\n * @param result the accumlated result\n */\nfunction arrayFlat(\n styles: CSSResultArray, result: CSSResult[] = []): CSSResult[] {\n for (let i = 0, length = styles.length; i < length; i++) {\n const value = styles[i];\n if (Array.isArray(value)) {\n arrayFlat(value, result);\n } else {\n result.push(value);\n }\n }\n return result;\n}\n\n/** Deeply flattens styles array. Uses native flat if available. */\nconst flattenStyles = (styles: CSSResultArray): CSSResult[] =>\n styles.flat ? styles.flat(Infinity) : arrayFlat(styles);\n\nexport class LitElement extends UpdatingElement {\n /**\n * Ensure this class is marked as `finalized` as an optimization ensuring\n * it will not needlessly try to `finalize`.\n */\n protected static finalized = true;\n\n /**\n * Render method used to render the lit-html TemplateResult to the element's\n * DOM.\n * @param {TemplateResult} Template to render.\n * @param {Element|DocumentFragment} Node into which to render.\n * @param {String} Element name.\n * @nocollapse\n */\n static render = render;\n\n /**\n * Array of styles to apply to the element. The styles should be defined\n * using the `css` tag function.\n */\n static styles?: CSSResult|CSSResultArray;\n\n private static _styles: CSSResult[]|undefined;\n\n /** @nocollapse */\n protected static finalize() {\n super.finalize();\n // Prepare styling that is stamped at first render time. Styling\n // is built from user provided `styles` or is inherited from the superclass.\n this._styles =\n this.hasOwnProperty(JSCompiler_renameProperty('styles', this)) ?\n this._getUniqueStyles() :\n this._styles || [];\n }\n\n /** @nocollapse */\n private static _getUniqueStyles(): CSSResult[] {\n // Take care not to call `this.styles` multiple times since this generates\n // new CSSResults each time.\n // TODO(sorvell): Since we do not cache CSSResults by input, any\n // shared styles will generate new stylesheet objects, which is wasteful.\n // This should be addressed when a browser ships constructable\n // stylesheets.\n const userStyles = this.styles;\n const styles: CSSResult[] = [];\n if (Array.isArray(userStyles)) {\n const flatStyles = flattenStyles(userStyles);\n // As a performance optimization to avoid duplicated styling that can\n // occur especially when composing via subclassing, de-duplicate styles\n // preserving the last item in the list. The last item is kept to\n // try to preserve cascade order with the assumption that it's most\n // important that last added styles override previous styles.\n const styleSet = flatStyles.reduceRight((set, s) => {\n set.add(s);\n // on IE set.add does not return the set.\n return set;\n }, new Set());\n // Array.from does not work on Set in IE\n styleSet.forEach((v) => styles!.unshift(v));\n } else if (userStyles) {\n styles.push(userStyles);\n }\n return styles;\n }\n\n private _needsShimAdoptedStyleSheets?: boolean;\n\n /**\n * Node or ShadowRoot into which element DOM should be rendered. Defaults\n * to an open shadowRoot.\n */\n protected renderRoot?: Element|DocumentFragment;\n\n /**\n * Performs element initialization. By default this calls `createRenderRoot`\n * to create the element `renderRoot` node and captures any pre-set values for\n * registered properties.\n */\n protected initialize() {\n super.initialize();\n this.renderRoot = this.createRenderRoot();\n // Note, if renderRoot is not a shadowRoot, styles would/could apply to the\n // element's getRootNode(). While this could be done, we're choosing not to\n // support this now since it would require different logic around de-duping.\n if (window.ShadowRoot && this.renderRoot instanceof window.ShadowRoot) {\n this.adoptStyles();\n }\n }\n\n /**\n * Returns the node into which the element should render and by default\n * creates and returns an open shadowRoot. Implement to customize where the\n * element's DOM is rendered. For example, to render into the element's\n * childNodes, return `this`.\n * @returns {Element|DocumentFragment} Returns a node into which to render.\n */\n protected createRenderRoot(): Element|ShadowRoot {\n return this.attachShadow({mode: 'open'});\n }\n\n /**\n * Applies styling to the element shadowRoot using the `static get styles`\n * property. Styling will apply using `shadowRoot.adoptedStyleSheets` where\n * available and will fallback otherwise. When Shadow DOM is polyfilled,\n * ShadyCSS scopes styles and adds them to the document. When Shadow DOM\n * is available but `adoptedStyleSheets` is not, styles are appended to the\n * end of the `shadowRoot` to [mimic spec\n * behavior](https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets).\n */\n protected adoptStyles() {\n const styles = (this.constructor as typeof LitElement)._styles!;\n if (styles.length === 0) {\n return;\n }\n // There are three separate cases here based on Shadow DOM support.\n // (1) shadowRoot polyfilled: use ShadyCSS\n // (2) shadowRoot.adoptedStyleSheets available: use it.\n // (3) shadowRoot.adoptedStyleSheets polyfilled: append styles after\n // rendering\n if (window.ShadyCSS !== undefined && !window.ShadyCSS.nativeShadow) {\n window.ShadyCSS.ScopingShim.prepareAdoptedCssText(\n styles.map((s) => s.cssText), this.localName);\n } else if (supportsAdoptingStyleSheets) {\n (this.renderRoot as ShadowRoot).adoptedStyleSheets =\n styles.map((s) => s.styleSheet!);\n } else {\n // This must be done after rendering so the actual style insertion is done\n // in `update`.\n this._needsShimAdoptedStyleSheets = true;\n }\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Note, first update/render handles styleElement so we only call this if\n // connected after first update.\n if (this.hasUpdated && window.ShadyCSS !== undefined) {\n window.ShadyCSS.styleElement(this);\n }\n }\n\n /**\n * Updates the element. This method reflects property values to attributes\n * and calls `render` to render DOM via lit-html. Setting properties inside\n * this method will *not* trigger another update.\n * * @param _changedProperties Map of changed properties with old values\n */\n protected update(changedProperties: PropertyValues) {\n super.update(changedProperties);\n const templateResult = this.render() as unknown;\n if (templateResult instanceof TemplateResult) {\n (this.constructor as typeof LitElement)\n .render(\n templateResult,\n this.renderRoot!,\n {scopeName: this.localName!, eventContext: this});\n }\n // When native Shadow DOM is used but adoptedStyles are not supported,\n // insert styling after rendering to ensure adoptedStyles have highest\n // priority.\n if (this._needsShimAdoptedStyleSheets) {\n this._needsShimAdoptedStyleSheets = false;\n (this.constructor as typeof LitElement)._styles!.forEach((s) => {\n const style = document.createElement('style');\n style.textContent = s.cssText;\n this.renderRoot!.appendChild(style);\n });\n }\n }\n\n /**\n * Invoked on each update to perform rendering tasks. This method must return\n * a lit-html TemplateResult. Setting properties inside this method will *not*\n * trigger the element to update.\n */\n protected render(): TemplateResult|void {\n }\n}\n"]} \ No newline at end of file diff --git a/app/userland/app-stdlib/vendor/lit-element/lit-html/CHANGELOG.md b/app/userland/app-stdlib/vendor/lit-element/lit-html/CHANGELOG.md new file mode 100755 index 0000000000..6499c444c6 --- /dev/null +++ b/app/userland/app-stdlib/vendor/lit-element/lit-html/CHANGELOG.md @@ -0,0 +1,194 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + + + + + + + + + +## [1.0.0] - 2019-02-05 +### Changed +* Tons of docs updates ([#746](https://github.com/Polymer/lit-html/pull/746)), ([#675](https://github.com/Polymer/lit-html/pull/675)), ([#724](https://github.com/Polymer/lit-html/pull/724)), ([#753](https://github.com/Polymer/lit-html/pull/753)), ([#764](https://github.com/Polymer/lit-html/pull/764)), ([#763](https://github.com/Polymer/lit-html/pull/763)), ([#765](https://github.com/Polymer/lit-html/pull/765)), ([#767](https://github.com/Polymer/lit-html/pull/767)), ([#768](https://github.com/Polymer/lit-html/pull/768)), ([#734](https://github.com/Polymer/lit-html/pull/734)), ([#771](https://github.com/Polymer/lit-html/pull/771)), ([#766](https://github.com/Polymer/lit-html/pull/766)), ([#773](https://github.com/Polymer/lit-html/pull/773)), ([#770](https://github.com/Polymer/lit-html/pull/770)), ([#769](https://github.com/Polymer/lit-html/pull/769)), ([#777](https://github.com/Polymer/lit-html/pull/777)), ([#776](https://github.com/Polymer/lit-html/pull/776)), ([#754](https://github.com/Polymer/lit-html/pull/754)), ([#779](https://github.com/Polymer/lit-html/pull/779)) +### Added +* Global version of `lit-html` on window ([#790](https://github.com/Polymer/lit-html/pull/790)). +### Fixed +* Removed use of `any` outside of test code ([#741](https://github.com/Polymer/lit-html/pull/741)). + +## [1.0.0-rc.2] - 2019-01-09 +### Changed +* Performance improvements to template processing. ([#690](https://github.com/Polymer/lit-html/pull/690)) +### Added +* Added the `nothing` sentinel value which can be used to clear a part. ([#673](https://github.com/Polymer/lit-html/pull/673)) +### Fixed +* Fixed #702: a bug with the `unsafeHTML` directive when changing between unsafe and other values. ([#703](https://github.com/Polymer/lit-html/pull/703)) +* Fixed #708: a bug with the `until` directive where placeholders could overwrite resolved Promises. ([#721](https://github.com/Polymer/lit-html/pull/721)) + + +## [1.0.0-rc.1] - 2018-12-13 +### Fixed +* Documentation updates. +* Fixed typing for template_polyfill `createElement` call. + +## [0.14.0] - 2018-11-30 +### Changed +* `until()` can now take any number of sync or async arguments. ([#555](https://github.com/Polymer/lit-html/pull/555)) +* [Breaking] `guard()` supports multiple dependencies. If the first argument to `guard()` is an array, the array items are checked for equality to previous values. ([#666](https://github.com/Polymer/lit-html/pull/666)) +* [Breaking] Renamed `classMap.js` and `styleMap.js` files to kebab-case. ([#644](https://github.com/Polymer/lit-html/pull/644)) +### Added +* Added `cache()` directive. ([#646](https://github.com/Polymer/lit-html/pull/646)) +* Removed Promise as a supposed node-position value type. ([#555](https://github.com/Polymer/lit-html/pull/555)) +* Added a minimal `