diff --git a/package.json b/package.json index df6fa3ef1..d97831e44 100644 --- a/package.json +++ b/package.json @@ -84,12 +84,14 @@ "colorjs.io": "^0", "decimal.js": "^10", "dexie": "^4.0.1-alpha.25", + "fuzzysort": "^2.0.4", "firebase": "^10.10.0", "firebase-functions": "^4.8.2", "graphemer": "^1.4.0", "matter-js": "^0.19.0", "pitchy": "^4.1.0", "recoverable-random": "^1.0.3", + "svelte-tiny-virtual-list": "^2.0.5", "uuid": "^9", "zod": "^3.22.4" }, diff --git a/src/components/editor/GlyphChooser.svelte b/src/components/editor/GlyphChooser.svelte index 32b21bc57..0682dcbed 100644 --- a/src/components/editor/GlyphChooser.svelte +++ b/src/components/editor/GlyphChooser.svelte @@ -1,110 +1,121 @@ -
- l.ui.source.cursor.search)} - bind:text={query} - /> -
- {#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 @@ + + + + + + + + + + + + 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",