(core) Detect when a page initial is an emoji, avoid repeating it, and style it better

Summary:
- Detecting emoji is surprisingly tricky; we use a fancy regex as a decent heuristic.
- Icons are a little larger than before.
- Styling tweaked for light and dark modes
- In case the OS doesn't render the emoji as one character, truncate what's
  shown in the icon box.

Test Plan: Added a test case.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3904
This commit is contained in:
Dmitry S
2023-06-26 12:58:29 -04:00
parent 4ea748b1a3
commit b0aa17c932
6 changed files with 93 additions and 9 deletions

View File

@@ -426,6 +426,9 @@ export const theme = {
undefined, colors.slate),
pageInitialsFg: new CustomProp('theme-left-panel-page-initials-fg', undefined, 'white'),
pageInitialsBg: new CustomProp('theme-left-panel-page-initials-bg', undefined, colors.slate),
pageInitialsEmojiBg: new CustomProp('theme-left-panel-page-emoji-fg', undefined, 'white'),
pageInitialsEmojiOutline: new CustomProp('theme-left-panel-page-emoji-outline', undefined,
colors.darkGrey),
/* Right Panel */
rightPanelTabFg: new CustomProp('theme-right-panel-tab-fg', undefined, colors.dark),

View File

@@ -6,7 +6,7 @@ import { theme } from "app/client/ui2018/cssVars";
import { icon } from "app/client/ui2018/icons";
import { hoverTooltip, overflowTooltip } from 'app/client/ui/tooltips';
import { menu, menuItem, menuText } from "app/client/ui2018/menus";
import { dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs";
import { Computed, dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs";
const t = makeT('pages');
@@ -54,17 +54,21 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
}
});
const splitName = Computed.create(null, name, (use, _name) => splitPageInitial(_name));
return pageElem = dom(
'div',
dom.autoDispose(lis),
dom.autoDispose(splitName),
domComputed((use) => use(name) === '', blank => blank ? dom('div', '-') :
domComputed(isRenaming, (isrenaming) => (
isrenaming ?
cssPageItem(
cssPageInitial(
testId('initial'),
dom.text((use) => Array.from(use(name))[0])
),
dom.text((use) => use(splitName).initial),
cssPageInitial.cls('-emoji', (use) => use(splitName).hasEmoji),
),
cssEditorInput(
{
initialValue: name.get() || '',
@@ -82,10 +86,11 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
cssPageItem(
cssPageInitial(
testId('initial'),
dom.text((use) => Array.from(use(name))[0]),
dom.text((use) => use(splitName).initial),
cssPageInitial.cls('-emoji', (use) => use(splitName).hasEmoji),
),
cssPageName(
dom.text(name),
dom.text((use) => use(splitName).displayName),
testId('label'),
dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)),
overflowTooltip(),
@@ -122,6 +127,24 @@ export function buildCensoredPage() {
);
}
// This crazy expression matches all "possible emoji" and comes from a very official source:
// https://unicode.org/reports/tr51/#EBNF_and_Regex (linked from
// https://stackoverflow.com/a/68146409/328565). It is processed from the original by replacing \x
// with \u, removing whitespace, and factoring out a long subexpression.
const emojiPart = /(?:\p{RI}\p{RI}|\p{Emoji}(?:\p{EMod}|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?)/u;
const pageInitialRegex = new RegExp(`^${emojiPart.source}(?:\\u{200D}${emojiPart.source})*`, "u");
// Divide up the page name into an "initial" and "displayName", where an emoji initial, if
// present, is omitted from the displayName, but a regular character used as the initial is kept.
function splitPageInitial(name: string): {initial: string, displayName: string, hasEmoji: boolean} {
const m = name.match(pageInitialRegex);
if (m) {
return {initial: m[0], displayName: name.slice(m[0].length).trim(), hasEmoji: true};
} else {
return {initial: Array.from(name)[0], displayName: name.trim(), hasEmoji: false};
}
}
const cssPageItem = styled('a', `
display: flex;
flex-direction: row;
@@ -129,7 +152,8 @@ const cssPageItem = styled('a', `
align-items: center;
flex-grow: 1;
.${treeViewContainer.className}-close & {
margin-left: 16px;
display: flex;
justify-content: center;
}
&, &:hover, &:focus {
text-decoration: none;
@@ -143,10 +167,25 @@ const cssPageInitial = styled('div', `
color: ${theme.pageInitialsFg};
border-radius: 3px;
background-color: ${theme.pageInitialsBg};
width: 16px;
height: 16px;
text-align: center;
width: 20px;
height: 20px;
margin-right: 8px;
display: flex;
justify-content: center;
align-items: center;
&-emoji {
background-color: ${theme.pageInitialsEmojiBg};
box-shadow: 0 0 0 1px var(--grist-theme-left-panel-page-emoji-outline, var(--grist-color-dark-grey));
font-size: 15px;
overflow: hidden;
}
.${treeViewContainer.className}-close & {
margin-right: 0;
}
.${itemHeader.className}.selected &-emoji {
box-shadow: none;
}
`);
const cssPageName = styled('div', `