diff --git a/package-lock.json b/package-lock.json
index d8121870f..0d3d11f67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "wordplay",
- "version": "0.16.13",
+ "version": "0.16.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wordplay",
- "version": "0.16.13",
+ "version": "0.16.14",
"bundleDependencies": [
"shared-types"
],
@@ -20,6 +20,7 @@
"firebase-admin": "^13",
"firebase-functions": "^6",
"firebase-functions-test": "^3",
+ "fuse.js": "^7.1.0",
"graphemer": "^1.4.0",
"matter-js": "^0.20",
"pitchy": "^4.1.0",
@@ -64,6 +65,7 @@
}
},
"functions/src/shared": {
+ "name": "shared-types",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
@@ -7133,6 +7135,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/fuse.js": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
+ "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
diff --git a/package.json b/package.json
index a63e97ccb..81a0e3280 100644
--- a/package.json
+++ b/package.json
@@ -49,10 +49,6 @@
"devDependencies": {
"@google-cloud/translate": "^9",
"@playwright/test": "^1",
- "svelte": "^5",
- "svelte-check": "^4",
- "svelte-jester": "^5",
- "svelte-preprocess": "^6.0",
"@sveltejs/adapter-static": "^3",
"@sveltejs/kit": "^2.20.6",
"@sveltejs/vite-plugin-svelte": "^5",
@@ -71,6 +67,10 @@
"prettier": "^3",
"prettier-plugin-svelte": "^3",
"run-script-os": "^1",
+ "svelte": "^5",
+ "svelte-check": "^4",
+ "svelte-jester": "^5",
+ "svelte-preprocess": "^6.0",
"ts-jest": "^29",
"ts-json-schema-generator": "^2",
"tslib": "^2",
@@ -90,13 +90,14 @@
"firebase-admin": "^13",
"firebase-functions": "^6",
"firebase-functions-test": "^3",
+ "fuse.js": "^7.1.0",
"graphemer": "^1.4.0",
"matter-js": "^0.20",
"pitchy": "^4.1.0",
"recoverable-random": "^1.0.5",
+ "shared-types": "file:./functions/src/shared",
"uuid": "^11",
- "zod": "^3.23.8",
- "shared-types": "file:./functions/src/shared"
+ "zod": "^3.23.8"
},
"browserslist": [
"defaults",
@@ -104,5 +105,8 @@
],
"bundledDependencies": [
"shared-types"
+ ],
+ "bundleDependencies": [
+ "shared-types"
]
}
diff --git a/src/components/app/ProjectPreview.svelte b/src/components/app/ProjectPreview.svelte
index dab6dec54..6762afe7f 100644
--- a/src/components/app/ProjectPreview.svelte
+++ b/src/components/app/ProjectPreview.svelte
@@ -35,6 +35,8 @@
children?: import('svelte').Snippet;
anonymize?: boolean;
showCollaborators?: boolean;
+ /** Search term for highlighting matches in project names */
+ searchTerm?: string;
}
function findCharacterName(value: Value): string | null {
@@ -71,6 +73,7 @@
children,
anonymize = true,
showCollaborators = false,
+ searchTerm = '',
}: Props = $props();
// Clone the project and get its initial value, then stop the project's evaluator.
@@ -166,6 +169,7 @@
const user = getUser();
let path = $derived(link ?? project.getLink(true));
+
/** See if this is a public project being viewed by someone who isn't a creator or collaborator */
let audience = $derived(isAudience($user, project));
@@ -223,14 +227,38 @@
{#if name}
{#if action}
- {project.getName()}
+ {#if searchTerm.trim()}
+ {@const name = project.getName()}
+ {@const searchLower = searchTerm.toLowerCase()}
+ {@const textLower = name.toLowerCase()}
+ {@const index = textLower.indexOf(searchLower)}
+ {#if index !== -1}
+ {name.substring(0, index)}
{name.substring(index, index + searchTerm.length)}{name.substring(index + searchTerm.length)}
+ {:else}
+ {name}
+ {/if}
+ {:else}
+ {project.getName()}
+ {/if}
{:else}
{#if project.getName().length === 0}
—
{:else}
- {project.getName()}{/if}{name.substring(index, index + searchTerm.length)}{name.substring(index + searchTerm.length)}
+ {:else}
+ {name}
+ {/if}
+ {:else}
+ {project.getName()}
+ {/if}{/if}
{#if navigating && `${navigating.to?.url.pathname}${navigating.to?.url.search}` === path}
{:else}{@render children?.()}
@@ -353,4 +381,12 @@
gap: var(--wordplay-spacing);
row-gap: var(--wordplay-spacing);
}
+
+ .search-highlight {
+ background-color: #ffffff;
+ color: #1f2937;
+ padding: 0 2px;
+ border-radius: 2px;
+ font-weight: 600;
+ }
\ No newline at end of file
diff --git a/src/components/app/ProjectPreviewSet.svelte b/src/components/app/ProjectPreviewSet.svelte
index 624b1b7c1..16dc33df8 100644
--- a/src/components/app/ProjectPreviewSet.svelte
+++ b/src/components/app/ProjectPreviewSet.svelte
@@ -34,6 +34,7 @@
children?: Snippet;
anonymize?: boolean;
showCollaborators?: boolean;
+ searchTerm?: string;
}
let {
@@ -44,6 +45,7 @@
children,
anonymize = true,
showCollaborators = false,
+ searchTerm = '',
}: Props = $props();
function sortProjects(projects: Project[]): Project[] {
@@ -63,6 +65,7 @@
link={project.getLink(true)}
{anonymize}
{showCollaborators}
+ {searchTerm}
>
{#if edit}