- {#if query === ''}
- {#each Defaults as command}
{/each}
- {:else}
- {#each results as glyph}
{:else}—{/each}
- {/if}
+
+
+
+
+
+ {#each Defaults as command}{/each}
+
+
+ {
+ expanded = true;
+ }}
+ bind:value={dropdownValue}
+ />
+
+
+
+ l.ui.source.toggle.glyphs)}
+ on={expanded}
+ toggle={() => {
+ expanded = !expanded;
+ }}>{expanded ? '–' : '+'}
+
+
+
+
- l.ui.source.toggle.glyphs)}
- on={expanded}
- toggle={() => (expanded = !expanded)}>{expanded ? '–' : '+'}
diff --git a/src/components/editor/GlyphSearchArea.svelte b/src/components/editor/GlyphSearchArea.svelte
new file mode 100644
index 000000000..4ed57352e
--- /dev/null
+++ b/src/components/editor/GlyphSearchArea.svelte
@@ -0,0 +1,214 @@
+
+
+
+
+
+
+ l.ui.source.cursor.search)}
+ fill
+ bind:text={query}
+ />
+
+
+
+
+
+
+
+ {#each recentlyUsed as glyph}
+
+
{/each}
+
+
+
+
+ {#if results.length > 0}
+
+ {#each getGlyphRow(index, category) as glyph}
+
+
+
+ {/each}
+
+ {:else}
+
No results found
+ {/if}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/widgets/DropdownButton.svelte b/src/components/widgets/DropdownButton.svelte
new file mode 100644
index 000000000..ad764b971
--- /dev/null
+++ b/src/components/widgets/DropdownButton.svelte
@@ -0,0 +1,428 @@
+
+
+
+
+
+
+
+
+
+
+
l.ui.source.toggle.glyphs)}
+ on={menuOpen}
+ fill={fill}
+ toggle={() => (menuOpen = !menuOpen)}
+ onKeyDown={onComboKeyDown}
+ >
+
+ {options[activeIndex]}
+ {menuOpen === openUp ? `▲` : `▼`}
+
+
+
+
+
+ {#each options as item, i}
+
+ {/each}
+
+
+
+
+
diff --git a/src/components/widgets/Label.svelte b/src/components/widgets/Label.svelte
new file mode 100644
index 000000000..13cf701dd
--- /dev/null
+++ b/src/components/widgets/Label.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/src/components/widgets/Toggle.svelte b/src/components/widgets/Toggle.svelte
index d8b5317cb..5a684d051 100644
--- a/src/components/widgets/Toggle.svelte
+++ b/src/components/widgets/Toggle.svelte
@@ -11,6 +11,10 @@
export let active = true;
export let uiid: string | undefined = undefined;
export let command: Command | undefined = undefined;
+ export let fill: boolean = false;
+
+ export let onBlur: ((event: FocusEvent) => void) | undefined = undefined
+ export let onKeyDown: ((event: KeyboardEvent) => void) | undefined = undefined;
async function doToggle(event: Event) {
if (active) {
@@ -33,10 +37,13 @@
data-uiid={uiid}
class:on
{title}
+ class:fill
aria-label={title}
aria-disabled={!active}
aria-pressed={on}
on:dblclick|stopPropagation
+ on:blur={onBlur}
+ on:keydown={onKeyDown}
on:mousedown|preventDefault
on:click={(event) =>
event.button === 0 && active ? doToggle(event) : undefined}
@@ -94,4 +101,8 @@
background: none;
color: var(--wordplay-inactive-color);
}
+
+ .fill {
+ width: 100%;
+ }
diff --git a/src/locale/UITexts.ts b/src/locale/UITexts.ts
index 4a9f484aa..c9eb98dfa 100644
--- a/src/locale/UITexts.ts
+++ b/src/locale/UITexts.ts
@@ -890,6 +890,13 @@ type UITexts = {
/** The placeholder string indicating that a template string could not be parsed */
unparsable: string;
};
+ /** Labels used throughout glyph picker */
+ label: {
+ /** The label for the quickly accessible operators */
+ operator: string;
+ /** The label for the recently used glyphs */
+ recent: string;
+ };
};
export { type UITexts as default };
diff --git a/src/locale/en-US.json b/src/locale/en-US.json
index 090ada8f1..c985a2758 100644
--- a/src/locale/en-US.json
+++ b/src/locale/en-US.json
@@ -4550,6 +4550,10 @@
"template": {
"unwritten": "TBD",
"unparsable": "Unparsable template: $1"
+ },
+ "label": {
+ "operator": "Operators",
+ "recent": "Recently Used"
}
},
"moderation": {
diff --git a/src/models/Project.ts b/src/models/Project.ts
index 80be43954..6bf91a3da 100644
--- a/src/models/Project.ts
+++ b/src/models/Project.ts
@@ -147,6 +147,7 @@ export default class Project {
flags: Moderation = moderatedFlags(),
// This is last; omitting it updates the time.
timestamp: number | undefined = undefined,
+ recentGlyphs: string[] = [],
) {
return new Project({
v: ProjectSchemaLatestVersion,
@@ -167,6 +168,7 @@ export default class Project {
archived,
persisted,
gallery,
+ recentGlyphs,
flags,
timestamp: timestamp ?? Date.now(),
nonPII: [],
@@ -806,6 +808,7 @@ export default class Project {
archived: project.archived,
persisted: project.persisted,
gallery: project.gallery,
+ recentGlyphs: project.recentGlyphs,
flags: { ...project.flags },
timestamp: project.timestamp,
nonPII: project.nonPII,
@@ -854,6 +857,14 @@ export default class Project {
return new Project({ ...this.data, gallery: id });
}
+ getRecentGlyphs() {
+ return this.data.recentGlyphs;
+ }
+
+ withRecentGlyphs(glyphs: string[]) {
+ return new Project({ ...this.data, recentGlyphs: glyphs });
+ }
+
getFlags() {
return { ...this.data.flags };
}
@@ -922,6 +933,7 @@ export default class Project {
persisted: this.isPersisted(),
timestamp: this.data.timestamp,
gallery: this.data.gallery,
+ recentGlyphs: this.data.recentGlyphs,
flags: { ...this.data.flags },
nonPII: this.data.nonPII,
};
diff --git a/src/models/ProjectSchemas.ts b/src/models/ProjectSchemas.ts
index 9ba4ee084..bb874143b 100644
--- a/src/models/ProjectSchemas.ts
+++ b/src/models/ProjectSchemas.ts
@@ -46,6 +46,8 @@ export const ProjectSchemaV1 = z.object({
persisted: z.boolean(),
/** An optional gallery ID, indicating which gallery this project is in. */
gallery: z.nullable(z.string()),
+ /** Recently used Glyphs for the project */
+ recentGlyphs: z.array(z.string()),
/** Moderation state */
flags: z.object({
dehumanization: z.nullable(z.boolean()),
diff --git a/src/unicode/Unicode.ts b/src/unicode/Unicode.ts
index 9015ee0a5..b85754c65 100644
--- a/src/unicode/Unicode.ts
+++ b/src/unicode/Unicode.ts
@@ -1,30 +1,67 @@
// Generated by unicode/compress.js. Run with Node.
import UnicodeDataTxt from './codes.txt?raw';
+import WordplayCategoryJson from './wordplay-categories.json';
+import fuzzysort from 'fuzzysort';
+
+export type WordplayCategories = 'emojis' | 'arrows' | 'shapes' | 'other';
type Codepoint = {
hex: number;
name: string;
- category: string;
+ unicodeCategory: string;
+ wordplayCategory: WordplayCategories;
emoji: { group: string; subgroup: string } | undefined;
};
const codepoints: Codepoint[] = [];
+const wordplayCategoryMap = WordplayCategoryJson as Record<
+ string,
+ WordplayCategories
+>;
for (const entry of UnicodeDataTxt.split('\n')) {
const [code, name, category, group, subgroup] = entry.split(';');
+
+ const isEmoji = group && subgroup;
+
codepoints.push({
hex: parseInt(code, 16),
name: name.toLowerCase(),
- category,
+ unicodeCategory: category,
+ wordplayCategory: isEmoji
+ ? 'emojis'
+ : wordplayCategoryMap[code] || 'other',
emoji: group && subgroup ? { group, subgroup } : undefined,
});
}
-export function getUnicodeNamed(name: string) {
+export function getUnicodeNamed(
+ name: string,
+ wordplayCategory?: WordplayCategories,
+ limit = 300,
+ all = true,
+) {
name = name.toLowerCase();
- return codepoints.filter((point) => point.name.includes(name));
+
+ const filteredCodepoints = codepoints.filter(
+ (point) => point.wordplayCategory === wordplayCategory,
+ );
+
+ const result =
+ name.length > 0
+ ? fuzzysort
+ .go(name, filteredCodepoints, {
+ key: 'name',
+ limit,
+ })
+ .map((result) => result.obj.hex)
+ : filteredCodepoints
+ .map((point) => point.hex)
+ .slice(0, all ? filteredCodepoints.length : limit);
+
+ return result;
}
export function getEmoji() {
- return codepoints.filter((point) => point.emoji !== undefined);
+ return codepoints.filter((point) => point.wordplayCategory === 'emojis');
}
diff --git a/src/unicode/wordplay-categories.json b/src/unicode/wordplay-categories.json
new file mode 100644
index 000000000..0d99df4c6
--- /dev/null
+++ b/src/unicode/wordplay-categories.json
@@ -0,0 +1,72 @@
+{
+ "231A": "emojis",
+ "231B": "emojis",
+ "23E9": "emojis",
+ "23FF": "emojis",
+ "2614": "emojis",
+ "2615": "emojis",
+ "2648": "emojis",
+ "2653": "emojis",
+ "267F": "emojis",
+ "26F2": "emojis",
+ "26F5": "emojis",
+ "26F7": "emojis",
+ "26FA": "emojis",
+ "26FD": "emojis",
+ "270A": "emojis",
+ "270D": "emojis",
+ "2728": "emojis",
+ "1F300": "emojis",
+ "1F531": "emojis",
+ "1F549": "emojis",
+ "1F57A": "emojis",
+ "1F58A": "emojis",
+ "1F58D": "emojis",
+ "1F5A5": "emojis",
+ "1F5A8": "emojis",
+ "1F5D1": "emojis",
+ "1F5D3": "emojis",
+ "1F5FA": "emojis",
+ "1F64F": "emojis",
+ "1F680": "emojis",
+ "1F6C5": "emojis",
+ "1F6CB": "emojis",
+ "1F6FC": "emojis",
+ "1F90C": "emojis",
+ "1F90F": "emojis",
+ "1F9FF": "emojis",
+ "1FA70": "emojis",
+ "1FA74": "emojis",
+ "1FA78": "emojis",
+ "1FA86": "emojis",
+ "1FA90": "emojis",
+ "1FAAC": "emojis",
+ "1FAB0": "emojis",
+ "1FABA": "emojis",
+ "1FAC0": "emojis",
+ "1FAC5": "emojis",
+ "1FAD0": "emojis",
+ "1FAF6": "emojis",
+ "21C4": "arrows",
+ "21F3": "arrows",
+ "2301": "arrows",
+ "2303": "arrows",
+ "2304": "arrows",
+ "25A0": "shapes",
+ "25D7": "shapes",
+ "25D9": "shapes",
+ "25F7": "shapes",
+ "2686": "shapes",
+ "2689": "shapes",
+ "26F6": "shapes",
+ "2729": "shapes",
+ "274B": "shapes",
+ "1F532": "shapes",
+ "1F53F": "shapes",
+ "1F7E0": "shapes",
+ "1F7EB": "shapes",
+ "1F90D": "shapes",
+ "1F90E": "shapes",
+ "1FA75": "shapes",
+ "1FA77": "shapes"
+}
diff --git a/static/locales/es-MX/es-MX.json b/static/locales/es-MX/es-MX.json
index 6ed8dea80..2e04773a4 100644
--- a/static/locales/es-MX/es-MX.json
+++ b/static/locales/es-MX/es-MX.json
@@ -4584,6 +4584,10 @@
"template": {
"unwritten": "Por determinar",
"unparsable": "Plantilla no analizable: $1"
+ },
+ "label": {
+ "operator": "Operadores",
+ "recent": "Usados Recientemente"
}
},
"moderation": {
diff --git a/static/locales/zh-CN/zh-CN.json b/static/locales/zh-CN/zh-CN.json
index 76153a219..ab7604e8c 100644
--- a/static/locales/zh-CN/zh-CN.json
+++ b/static/locales/zh-CN/zh-CN.json
@@ -4394,6 +4394,10 @@
"template": {
"unwritten": "待定",
"unparsable": "无法解析的模板: $1"
+ },
+ "label": {
+ "operator": "算子",
+ "recent": "最近使用"
}
},
"moderation": {
diff --git a/static/schemas/Locale.json b/static/schemas/Locale.json
index 5095f6b7a..54fb70009 100644
--- a/static/schemas/Locale.json
+++ b/static/schemas/Locale.json
@@ -10646,6 +10646,24 @@
],
"type": "object"
},
+ "label": {
+ "additionalProperties": false,
+ "properties": {
+ "operator": {
+ "description": "The label for the quickly accessible operators",
+ "type": "string"
+ },
+ "recent": {
+ "description": "The label for the recently used glyphs",
+ "type": "string"
+ }
+ },
+ "required": [
+ "operator",
+ "recent"
+ ],
+ "type": "object"
+ },
"tile": {
"additionalProperties": false,
"description": "Controls for the tiled windows in the project",