diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index fa37047bf..1209d856f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -292,8 +292,8 @@ importers: specifier: ^2.12.0 version: 2.13.0 svelte-ux: - specifier: ^0.66.8 - version: 0.66.8(@babel/core@7.24.7)(postcss-load-config@4.0.2(postcss@8.4.47))(postcss@8.4.47)(svelte@4.2.19) + specifier: ^0.74.9 + version: 0.74.9(@babel/core@7.24.7)(postcss-load-config@4.0.2(postcss@8.4.47))(postcss@8.4.47)(svelte@4.2.19) type-fest: specifier: ^4.18.2 version: 4.18.2 @@ -911,12 +911,21 @@ packages: '@floating-ui/core@1.6.0': resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} + '@floating-ui/core@1.6.8': + resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + + '@floating-ui/dom@1.6.11': + resolution: {integrity: sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==} + '@floating-ui/dom@1.6.5': resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} '@floating-ui/utils@0.2.1': resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + '@formatjs/ecma402-abstract@1.18.2': resolution: {integrity: sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==} @@ -929,8 +938,8 @@ packages: '@formatjs/intl-localematcher@0.5.4': resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} - '@fortawesome/fontawesome-common-types@6.5.2': - resolution: {integrity: sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==} + '@fortawesome/fontawesome-common-types@6.6.0': + resolution: {integrity: sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==} engines: {node: '>=6'} '@graphql-codegen/add@5.0.2': @@ -2233,6 +2242,9 @@ packages: '@types/node@20.12.12': resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} + '@types/node@22.7.3': + resolution: {integrity: sha512-qXKfhXXqGTyBskvWEzJZPUxSslAiLaB6JGP1ic/XTH9ctGgzdgYguuLP1C601aRTSDNlLb0jbKqXjZ48GNraSA==} + '@types/pg-pool@2.0.4': resolution: {integrity: sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==} @@ -2437,6 +2449,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.0: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} @@ -5153,8 +5170,8 @@ packages: peerDependencies: svelte: ^3.58.0 || ^4.0.0 - svelte-ux@0.66.8: - resolution: {integrity: sha512-PaDLLSHnktN1uv/Viu8joHidvQ2UPh3AEB8JxeWgGH/GPwirYfa7jAferad+TMrv5Cvp9V124T559ySaLThxJg==} + svelte-ux@0.74.9: + resolution: {integrity: sha512-df1F5EMPbjqGkbBuuHMmgRHp+gQspLrbQjF3foyzWss6gsRkZwFba0JkaopOwQNMMWb3Zu+2obIgQJXt1eZ/Dg==} peerDependencies: svelte: ^3.56.0 || ^4.0.0 || ^5.0.0-next.120 @@ -5178,8 +5195,8 @@ packages: swap-case@2.0.2: resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} - tailwind-merge@2.3.0: - resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} + tailwind-merge@2.5.2: + resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==} tailwindcss@3.4.3: resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==} @@ -5307,6 +5324,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + engines: {node: '>=14.17'} + hasBin: true + ua-parser-js@1.0.37: resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} @@ -5328,6 +5350,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} @@ -6290,6 +6315,15 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.1 + '@floating-ui/core@1.6.8': + dependencies: + '@floating-ui/utils': 0.2.8 + + '@floating-ui/dom@1.6.11': + dependencies: + '@floating-ui/core': 1.6.8 + '@floating-ui/utils': 0.2.8 + '@floating-ui/dom@1.6.5': dependencies: '@floating-ui/core': 1.6.0 @@ -6297,6 +6331,8 @@ snapshots: '@floating-ui/utils@0.2.1': {} + '@floating-ui/utils@0.2.8': {} + '@formatjs/ecma402-abstract@1.18.2': dependencies: '@formatjs/intl-localematcher': 0.5.4 @@ -6317,7 +6353,7 @@ snapshots: dependencies: tslib: 2.6.2 - '@fortawesome/fontawesome-common-types@6.5.2': {} + '@fortawesome/fontawesome-common-types@6.6.0': {} '@graphql-codegen/add@5.0.2(graphql@16.8.1)': dependencies: @@ -7963,6 +7999,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@22.7.3))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@22.7.3)))(svelte@4.2.19)(vite@5.4.8(@types/node@22.7.3)) + debug: 4.3.4 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.10 + svelte: 4.2.19 + svelte-hmr: 0.16.0(svelte@4.2.19) + vite: 5.4.8(@types/node@22.7.3) + vitefu: 0.2.5(vite@5.4.8(@types/node@22.7.3)) + transitivePeerDependencies: + - supports-color + '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.3)': dependencies: tailwindcss: 3.4.3 @@ -8104,6 +8154,11 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@22.7.3': + dependencies: + undici-types: 6.19.8 + optional: true + '@types/pg-pool@2.0.4': dependencies: '@types/pg': 8.6.1 @@ -8280,6 +8335,10 @@ snapshots: dependencies: vite: 5.4.9(@types/node@20.12.12) + '@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.8(@types/node@22.7.3))': + dependencies: + vite: 5.4.8(@types/node@22.7.3) + '@vitest/expect@1.6.0': dependencies: '@vitest/spy': 1.6.0 @@ -8360,6 +8419,8 @@ snapshots: acorn@8.11.3: {} + acorn@8.12.1: {} + agent-base@7.1.0: dependencies: debug: 4.3.5 @@ -11475,8 +11536,8 @@ snapshots: rollup: 2.79.2 rollup-plugin-svelte: 7.2.2(rollup@2.79.2)(svelte@4.2.19) svelte: 4.2.19 - svelte-preprocess: 5.1.4(@babel/core@7.24.7)(postcss-load-config@4.0.2(postcss@8.4.47))(postcss@8.4.47)(svelte@4.2.19)(typescript@5.3.3) - typescript: 5.3.3 + svelte-preprocess: 5.1.4(@babel/core@7.24.7)(postcss-load-config@4.0.2(postcss@8.4.47))(postcss@8.4.47)(svelte@4.2.19)(typescript@5.6.2) + typescript: 5.6.2 transitivePeerDependencies: - '@babel/core' - coffeescript @@ -11606,6 +11667,20 @@ snapshots: postcss-load-config: 4.0.2(postcss@8.4.47) typescript: 5.3.3 + svelte-preprocess@5.1.4(@babel/core@7.24.7)(postcss-load-config@4.0.2(postcss@8.4.47))(postcss@8.4.47)(svelte@4.2.19)(typescript@5.6.2): + dependencies: + '@types/pug': 2.0.10 + detect-indent: 6.1.0 + magic-string: 0.30.10 + sorcery: 0.11.0 + strip-indent: 3.0.0 + svelte: 4.2.19 + optionalDependencies: + '@babel/core': 7.24.7 + postcss: 8.4.47 + postcss-load-config: 4.0.2(postcss@8.4.47) + typescript: 5.6.2 + svelte-routing@2.13.0: {} svelte-turnstile@0.5.0(svelte@4.2.19): @@ -11613,10 +11688,10 @@ snapshots: svelte: 4.2.19 turnstile-types: 1.2.0 - svelte-ux@0.66.8(@babel/core@7.24.7)(postcss-load-config@4.0.2(postcss@8.4.47))(postcss@8.4.47)(svelte@4.2.19): + svelte-ux@0.74.9(@babel/core@7.24.7)(postcss-load-config@4.0.2(postcss@8.4.47))(postcss@8.4.47)(svelte@4.2.19): dependencies: - '@floating-ui/dom': 1.6.5 - '@fortawesome/fontawesome-common-types': 6.5.2 + '@floating-ui/dom': 1.6.11 + '@fortawesome/fontawesome-common-types': 6.6.0 '@mdi/js': 7.4.47 clsx: 2.1.1 culori: 4.0.1 @@ -11630,7 +11705,7 @@ snapshots: prismjs: 1.29.0 sveld: 0.20.0(@babel/core@7.24.7)(postcss-load-config@4.0.2(postcss@8.4.47))(postcss@8.4.47) svelte: 4.2.19 - tailwind-merge: 2.3.0 + tailwind-merge: 2.5.2 zod: 3.23.8 transitivePeerDependencies: - '@babel/core' @@ -11681,9 +11756,7 @@ snapshots: dependencies: tslib: 2.6.2 - tailwind-merge@2.3.0: - dependencies: - '@babel/runtime': 7.24.1 + tailwind-merge@2.5.2: {} tailwindcss@3.4.3: dependencies: @@ -11813,6 +11886,8 @@ snapshots: typescript@5.3.3: {} + typescript@5.6.2: {} + ua-parser-js@1.0.37: {} ufo@1.3.2: {} @@ -11825,6 +11900,9 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.19.8: + optional: true + undici@5.28.4: dependencies: '@fastify/busboy': 2.1.1 @@ -11965,6 +12043,10 @@ snapshots: optionalDependencies: vite: 5.4.9(@types/node@20.12.12) + vitefu@0.2.5(vite@5.4.8(@types/node@22.7.3)): + optionalDependencies: + vite: 5.4.8(@types/node@22.7.3) + vitest@1.6.0(@types/node@20.12.12): dependencies: '@vitest/expect': 1.6.0 diff --git a/frontend/viewer/package.json b/frontend/viewer/package.json index 0b19bfda6..c38851952 100644 --- a/frontend/viewer/package.json +++ b/frontend/viewer/package.json @@ -47,7 +47,7 @@ "svelte-exmarkdown": "^3.0.5", "svelte-preprocess": "catalog:", "svelte-routing": "^2.12.0", - "svelte-ux": "^0.66.8", + "svelte-ux": "^0.74.9", "type-fest": "^4.18.2" } } diff --git a/frontend/viewer/src/App.svelte b/frontend/viewer/src/App.svelte index 60aeae4f9..07cae2605 100644 --- a/frontend/viewer/src/App.svelte +++ b/frontend/viewer/src/App.svelte @@ -6,9 +6,37 @@ import HomeView from './HomeView.svelte'; import NotificationOutlet from './lib/notifications/NotificationOutlet.svelte'; import Sandbox from './lib/sandbox/Sandbox.svelte'; + import { settings } from 'svelte-ux'; export let url = ''; + settings({ + components: { + MenuItem: { + classes: { + root: 'justify-end', + }, + }, + ListItem: { + classes: { + root: 'cursor-pointer hover:bg-surface-300 hover:border-surface-300 overflow-hidden', + subheading: 'whitespace-nowrap overflow-hidden overflow-x-clip text-ellipsis', + } + }, + ExpansionPanel: { + classes: { + toggle: 'p-0', + }, + }, + Collapse: { + classes: { + content: 'CollapseContent', + icon: 'CollapseIcon', + } + } + }, + }); + @@ -16,17 +44,23 @@
- {#key params.name} - - {/key} + + {#key params.name} + + {/key} + - {#key params.name} - - {/key} + + {#key params.name} + + {/key} + - + + + diff --git a/frontend/viewer/src/ProjectView.svelte b/frontend/viewer/src/ProjectView.svelte index 7a83aa146..89f99a328 100644 --- a/frontend/viewer/src/ProjectView.svelte +++ b/frontend/viewer/src/ProjectView.svelte @@ -9,8 +9,8 @@ mdiHome } from '@mdi/js'; import Editor from './lib/Editor.svelte'; - import {navigate} from 'svelte-routing'; - import {headword, pickBestAlternative} from './lib/utils'; + import {navigate, useLocation} from 'svelte-routing'; + import {headword} from './lib/utils'; import {useLexboxApi} from './lib/services/service-provider'; import type {IEntry} from './lib/mini-lcm'; import {onDestroy, onMount, setContext} from 'svelte'; @@ -25,9 +25,8 @@ import NewEntryDialog from './lib/entry-editor/NewEntryDialog.svelte'; import SearchBar from './lib/search-bar/SearchBar.svelte'; import ActivityView from './lib/activity/ActivityView.svelte'; - import type { OptionProvider } from './lib/services/option-provider'; import { getAvailableHeightForElement } from './lib/utils/size'; - import { ViewerSearchParam, getSearchParam, updateSearchParam } from './lib/utils/search-params'; + import { ViewerSearchParam, getSearchParam, getSearchParams, updateSearchParam } from './lib/utils/search-params'; import SaveStatus from './lib/status/SaveStatus.svelte'; import { saveEventDispatcher, saveHandler } from './lib/services/save-event-service'; import {AppNotification} from './lib/notifications/notifications'; @@ -37,6 +36,7 @@ import {initWritingSystems} from './lib/writing-systems'; import {useEventBus} from './lib/services/event-bus'; import AboutDialog from './lib/about/AboutDialog.svelte'; + import { initProjectCommands, type NewEntryDialogOptions } from './lib/commands'; export let loading = false; @@ -83,12 +83,20 @@ $: connected.set(isConnected); const connected = writable(false); - const search = writable(getSearchParam(ViewerSearchParam.Search)); + const search = writable(getSearchParam(ViewerSearchParam.Search) ?? undefined); setContext('listSearch', search); $: updateSearchParam(ViewerSearchParam.Search, $search, true); - //todo listen for changes to the url, for example when back/forward is pressed - const selectedIndexExemplar = writable(getSearchParam(ViewerSearchParam.IndexCharacter)); + const location = useLocation(); + $: { + $location; + const searchParams = getSearchParams(); + $search = searchParams.get(ViewerSearchParam.Search) ?? ''; + $selectedIndexExemplar = searchParams.get(ViewerSearchParam.IndexCharacter); + navigateToEntryId = searchParams.get(ViewerSearchParam.EntryId); + } + + const selectedIndexExemplar = writable(getSearchParam(ViewerSearchParam.IndexCharacter)); setContext('selectedIndexExamplar', selectedIndexExemplar); $: updateSearchParam(ViewerSearchParam.IndexCharacter, $selectedIndexExemplar, false); @@ -120,12 +128,13 @@ // Used for triggering rerendering when display values of the current entry change (e.g. the headword in the list view) const entries = writable(); $: $entries = $_entries; + $: console.debug('Entries:', $_entries); - function fetchEntries(s: string, isConnected: boolean, exemplar: string | undefined) { + function fetchEntries(s: string, isConnected: boolean, exemplar: string | null) { if (!isConnected) return Promise.resolve(undefined); return lexboxApi.SearchEntries(s ?? '', { offset: 0, - // we always load full exampelar lists for now, so we can guaruntee that the selected entry is in the list + // we always load full exemplar lists for now, so we can guaruntee that the selected entry is in the list count: exemplar ? 1_000_000_000 : 1000, order: {field: 'headword', writingSystem: 'default'}, exemplar: exemplar ? {value: exemplar, writingSystem: 'default'} : undefined @@ -134,7 +143,7 @@ let showOptionsDialog = false; let pickedEntry = false; - let navigateToEntryIdOnLoad = getSearchParam(ViewerSearchParam.EntryId); + let navigateToEntryId = getSearchParam(ViewerSearchParam.EntryId); const selectedEntry = writable(undefined); setContext('selectedEntry', selectedEntry); // For some reason reactive syntax doesn't pick up every change, so we need to manually subscribe @@ -142,7 +151,7 @@ const unsubSelectedEntry = selectedEntry.subscribe(updateEntryIdSearchParam); $: { pickedEntry; updateEntryIdSearchParam(); } function updateEntryIdSearchParam() { - updateSearchParam(ViewerSearchParam.EntryId, navigateToEntryIdOnLoad ?? (pickedEntry ? $selectedEntry?.id : undefined), true); + updateSearchParam(ViewerSearchParam.EntryId, navigateToEntryId ?? (pickedEntry ? $selectedEntry?.id : undefined), true); } $: { @@ -154,14 +163,14 @@ function refreshSelection() { if (!$entries) return; - if ($selectedEntry !== undefined) { - const entry = $entries.find(e => e.id === $selectedEntry!.id); - if (entry !== $selectedEntry) { + if (navigateToEntryId) { + const entry = $entries.find(e => e.id === navigateToEntryId); + if (entry) { $selectedEntry = entry; } - } else if (navigateToEntryIdOnLoad) { - const entry = $entries.find(e => e.id === navigateToEntryIdOnLoad); - if (entry) { + } else if ($selectedEntry !== undefined) { + const entry = $entries.find(e => e.id === $selectedEntry!.id); + if (entry !== $selectedEntry) { $selectedEntry = entry; } } @@ -175,20 +184,22 @@ } updateEntryIdSearchParam(); - navigateToEntryIdOnLoad = undefined; + navigateToEntryId = null; } $: _loading = !$entries || !$writingSystems || loading; - function onEntryCreated(entry: IEntry) { + function onEntryCreated(entry: IEntry, options?: NewEntryDialogOptions) { $entries?.push(entry);//need to add it before refresh, otherwise it won't get selected because it's not in the list - navigateToEntry(entry, headword(entry)); + if (!options?.dontNavigate) { + navigateToEntry(entry, headword(entry)); + } } function navigateToEntry(entry: IEntry, searchText?: string) { // this is to ensure that the selected entry is in the list of entries, otherwise it won't be selected $search = searchText ?? ''; - $selectedIndexExemplar = undefined; + $selectedIndexExemplar = null; $selectedEntry = entry; refreshEntries(); pickedEntry = true; @@ -227,12 +238,19 @@ callback: () => window.location.reload() }); } + let newEntryDialog: NewEntryDialog; - function openNewEntryDialog(text: string) { + async function openNewEntryDialog(text: string, options?: NewEntryDialogOptions): Promise { const defaultWs = $writingSystems?.vernacular[0].id; - if (defaultWs === undefined) return; - newEntryDialog.openWithValue({lexemeForm: {[defaultWs]: text}}); + if (defaultWs === undefined) return undefined; + const entry = await newEntryDialog.openWithValue({lexemeForm: {[defaultWs]: text}}); + if (entry) onEntryCreated(entry, options); + return entry; } + + initProjectCommands({ + createNewEntry: openNewEntryDialog, + }); @@ -273,7 +291,7 @@
{#if !readonly} - onEntryCreated(e.detail.entry)} /> + {/if} - - {/each} - - - {#if first} - - {:else if last} - - {:else} - - {/if} - -
+ + + newIndex = i}> + {#each displayItems as item, j} + + + {/each} + + + {#if first} + + {:else if last} + + {:else} + + {/if} + {/if} {#if $features.history && id} diff --git a/frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte b/frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte new file mode 100644 index 000000000..d4a49b995 --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/EntryOrSenseItemList.svelte @@ -0,0 +1,28 @@ + + + `?entryId=${getEntryId(entry)}&search=${getHeadword(entry)?.replace(/\d?$/, '')}`}> + + + + Go to {getHeadword(entry) || '–'} + + + + + + diff --git a/frontend/viewer/src/lib/entry-editor/EntryOrSensePicker.postcss b/frontend/viewer/src/lib/entry-editor/EntryOrSensePicker.postcss new file mode 100644 index 000000000..29e5e63d2 --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/EntryOrSensePicker.postcss @@ -0,0 +1,48 @@ +/* These styles belong in EntryOrSensePicker.svelte, but the mixture of global and features like :dark and :is, are buggy if they're there 😞 */ +.entry-sense-picker { + + .Collapse { + @apply border-2 !border-t-2 border-surface-100 rounded overflow-hidden elevation-0 m-0 transition-none; + } + + .entry:not(.disabled.disable-expand) .Collapse { + button:hover { + @apply bg-surface-300; + & * { + @apply bg-transparent; + } + } + } + + /* We need this, because there's a bug that adds aria-expanded="true" even when the expansion panel is disabled */ + .entry:not(.disable-expand) .Collapse[aria-expanded="true"] { + @apply border-surface-300; + } + + .CollapseContent .ListItem { + @apply !rounded-none; + } + + .entry:is(.selected, :has(.selected)) .Collapse { + @apply !border-accent-500; + } + + .selected.sense, .selected.entry button:not(.sense) { + @apply !bg-accent-100; + @apply dark:!bg-accent-900; + + & > * { + @apply bg-transparent; + } + } + + .disabled { + &.entry [slot="trigger"] { + @apply brightness-50 bg-surface-100 pointer-events-none; + } + + &.sense { + @apply brightness-50 pointer-events-none; + } + } +} diff --git a/frontend/viewer/src/lib/entry-editor/EntryOrSensePicker.svelte b/frontend/viewer/src/lib/entry-editor/EntryOrSensePicker.svelte new file mode 100644 index 000000000..e6187ab58 --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/EntryOrSensePicker.svelte @@ -0,0 +1,246 @@ + + + + + +
+

+ {title} +

+ +
+ {#if $loading} + + {/if} +
+
+
+
+ {#each [...$displayedEntries, ...addedEntries] as entry (entry.id)} + {@const disabledEntry = disableEntry?.(entry)} + {@const disableExpand = onlyEntries || (disabledEntry && disabledEntry.disableSenses)} +
+ onExpansionChange(event.detail.open, entry, !!disabledEntry)} + > + + {#each entry.senses as sense} + {@const disabledSense = disableSense?.(sense, entry)} + + + {/each} + onClickAddSense(entry)} + /> + +
+ {/each} + {#if $displayedEntries.length === 0 && addedEntries.length === 0} +
+ {#if $result.search} + No entries found + {:else if $loading} + + {:else} + Search for an entry {onlyEntries ? '' : 'or sense'} + {/if} +
+ {/if} + {#if $result.search} + onClickCreateNewEntry($result.search)} + /> + {/if} + {#if $result.entries.length > $displayedEntries.length} +
+ {$result.entries.length - $displayedEntries.length} + {#if $result.entries.length === fetchCount}+{/if} +
+ more matching entries... +
+
+ {/if} +
+
+
+ + +
+
diff --git a/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte b/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte index 6d2a26f65..a8526df16 100644 --- a/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte +++ b/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte @@ -9,7 +9,7 @@ import type { SaveHandler } from '../services/save-event-service'; const dispatch = createEventDispatcher<{ - created: { entry: IEntry}; + created: { entry: IEntry }; }>(); let loading = false; @@ -17,25 +17,45 @@ const lexboxApi = useLexboxApi(); const saveHandler = getContext('saveHandler'); + let requester: { + resolve: (entry: IEntry | undefined) => void + } | undefined; async function createEntry(e: Event, closeDialog: () => void) { e.preventDefault(); loading = true; await saveHandler(() => lexboxApi.CreateEntry(entry)); dispatch('created', {entry}); + if (requester) { + requester.resolve(entry); + requester = undefined; + } loading = false; closeDialog(); } - export function openWithValue(newEntry: Partial) { - const tmpEntry = defaultEntry(); - toggle.on = true; - entry = {...tmpEntry, ...newEntry, id: tmpEntry.id}; + export function openWithValue(newEntry: Partial): Promise { + return new Promise((resolve) => { + if (requester) requester.resolve(undefined); + requester = { resolve }; + const tmpEntry = defaultEntry(); + toggle.on = true; + entry = {...tmpEntry, ...newEntry, id: tmpEntry.id}; + }); } + + function onClosing() { + if (requester) { + requester.resolve(undefined); + requester = undefined; + } + entry = defaultEntry(); + } + let toggle: Toggle; - entry = defaultEntry()}> + + + {/each} + + + {#if first || last} + + {:else} + + {/if} + + + {/if} + { + remove(item); + closeMenu(); + }}> + Remove + + + {/if} + + + +
+ {/each} + {#if !readonly} +
+ +
+ {/if} + diff --git a/frontend/viewer/src/lib/entry-editor/field-data.ts b/frontend/viewer/src/lib/entry-editor/field-data.ts index 79faea99b..88f30cb1f 100644 --- a/frontend/viewer/src/lib/entry-editor/field-data.ts +++ b/frontend/viewer/src/lib/entry-editor/field-data.ts @@ -7,6 +7,9 @@ const privateFieldData = { //entry lexemeForm: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Lexeme_Form_field.htm' }, citationForm: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Citation_Form_field.htm' }, + complexForms: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Complex_Forms.htm' }, + complexFormTypes: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Complex_Form_Type_field.htm' }, + components: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Components_field.htm' }, literalMeaning: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Literal_Meaning_field.htm' }, note: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Note_field.htm' }, diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormComponents.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormComponents.svelte new file mode 100644 index 000000000..46232087b --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormComponents.svelte @@ -0,0 +1,72 @@ + + +
+ +
+ dispatch('change', { value })} getEntryId={(e) => e.componentEntryId} getHeadword={(e) => e.componentHeadword}> + + + addComponent(e.detail)} + {disableEntry} {disableSense} /> + + +
+
diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormTypes.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormTypes.svelte new file mode 100644 index 000000000..2d576c0b5 --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormTypes.svelte @@ -0,0 +1,53 @@ + + +
+ + +
+ dispatch('change', { value })} getDisplayName={(type) => firstVal(type.name)}> + + + + + +
+
diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/ComplexForms.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexForms.svelte new file mode 100644 index 000000000..f079055cd --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexForms.svelte @@ -0,0 +1,66 @@ + + +
+ +
+ dispatch('change', { value })} getEntryId={(e) => e.complexFormEntryId} getHeadword={(e) => e.complexFormHeadword}> + + + addComplexForm(e.detail)} + {disableEntry} /> + + +
+
diff --git a/frontend/viewer/src/lib/entry-editor/field.postcss b/frontend/viewer/src/lib/entry-editor/field.postcss index 832255d59..91e7dec82 100644 --- a/frontend/viewer/src/lib/entry-editor/field.postcss +++ b/frontend/viewer/src/lib/entry-editor/field.postcss @@ -20,6 +20,10 @@ grid-column-start: 3; } + .item-list-field { + @apply col-span-2 pl-4; + } + .readonly { &.TextField, .TextField { opacity: 1; diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte index 7d7821196..8e14f50e1 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte @@ -13,6 +13,9 @@ import ExampleEditor from './ExampleEditor.svelte'; import MultiFieldEditor from '../field-editors/MultiFieldEditor.svelte'; import {objectTemplateAreas, useCurrentView} from '../../services/view-service'; + import ComplexFormComponents from '../field-editors/ComplexFormComponents.svelte'; + import ComplexForms from '../field-editors/ComplexForms.svelte'; + import ComplexFormTypes from '../field-editors/ComplexFormTypes.svelte'; const dispatch = createEventDispatcher<{ change: { entry: IEntry, sense?: ISense, example?: IExampleSentence}; @@ -105,17 +108,36 @@
-
+
dispatch('change', {entry})} bind:value={entry.lexemeForm} {readonly} id="lexemeForm" wsType="vernacular"/> + dispatch('change', {entry})} bind:value={entry.citationForm} {readonly} id="citationForm" wsType="vernacular"/> + + dispatch('change', {entry})} + bind:value={entry.complexForms} + {readonly} + {entry} + id="complexForms" /> + + dispatch('change', {entry})} + bind:value={entry.complexFormTypes} + {readonly} + id="complexFormTypes" /> + + dispatch('change', {entry})} + bind:value={entry.components} + {readonly} + {entry} + id="components" /> + dispatch('change', {entry})} bind:value={entry.literalMeaning} {readonly} diff --git a/frontend/viewer/src/lib/entry-editor/view-data.ts b/frontend/viewer/src/lib/entry-editor/view-data.ts index 7f011993e..d26a4b53c 100644 --- a/frontend/viewer/src/lib/entry-editor/view-data.ts +++ b/frontend/viewer/src/lib/entry-editor/view-data.ts @@ -10,8 +10,11 @@ export const allFields: Record = { //entry lexemeForm: {show: true, order: 1}, citationForm: {show: true, order: 2}, - literalMeaning: {show: true, order: 3}, - note: {show: true, order: 4}, + complexForms: {show: true, order: 3}, + complexFormTypes: {show: true, order: 4}, + components: {show: true, order: 5}, + literalMeaning: {show: true, order: 6}, + note: {show: true, order: 7}, //sense gloss: {show: true, order: 1}, diff --git a/frontend/viewer/src/lib/history/HistoryView.svelte b/frontend/viewer/src/lib/history/HistoryView.svelte index 318005401..6f6dc9042 100644 --- a/frontend/viewer/src/lib/history/HistoryView.svelte +++ b/frontend/viewer/src/lib/history/HistoryView.svelte @@ -72,11 +72,7 @@ title={row.changeName ?? 'No change name'} on:click={() => showEntry(row)} noShadow - class={cls( - 'cursor-pointer', - 'hover:bg-surface-300', - record?.commitId === row.commitId ? 'bg-surface-200 selected-entry' : '' - )}> + class={cls(record?.commitId === row.commitId ? 'bg-surface-200 selected-entry' : '')}>
{#if row.previousTimestamp} & Record, string>; @@ -11,6 +12,9 @@ export type I18nType = 'weSay' | 'languageForge' | ''; const defaultI18n: Record = { 'lexemeForm': 'Lexeme form', 'citationForm': 'Citation form', + 'complexForms': 'Complex Forms', + 'complexFormTypes': 'Complex Form Types', + 'components': 'Components', 'literalMeaning': 'Literal meaning', 'note': 'Note', 'definition': 'Definition', @@ -27,12 +31,16 @@ const weSayI18n = { 'lexemeForm': 'Word', 'gloss': 'Definition', 'partOfSpeechId': 'Part of speech', -}; + complexForms: 'Part of', + components: 'Made of', +} satisfies Partial>; const languageForgeI18n = { 'lexemeForm': 'Word', 'partOfSpeechId': 'Part of speech', -}; + complexForms: 'Part of', + components: 'Made of', +} satisfies Partial>; const i18nMap: Record, Partial>> = { weSay: weSayI18n, diff --git a/frontend/viewer/src/lib/in-memory-api-service.ts b/frontend/viewer/src/lib/in-memory-api-service.ts index 5eaac78c4..986e79379 100644 --- a/frontend/viewer/src/lib/in-memory-api-service.ts +++ b/frontend/viewer/src/lib/in-memory-api-service.ts @@ -44,7 +44,9 @@ export class InMemoryApiService implements LexboxApiClient { } SupportedFeatures(): LexboxApiFeatures { - return {}; + return { + write: true, + }; } readonly projectName = projectName; diff --git a/frontend/viewer/src/lib/layout/EntryList.svelte b/frontend/viewer/src/lib/layout/EntryList.svelte index d6bf68569..395418d2f 100644 --- a/frontend/viewer/src/lib/layout/EntryList.svelte +++ b/frontend/viewer/src/lib/layout/EntryList.svelte @@ -22,7 +22,7 @@ // wait until the new entries have been rendered setTimeout(() => { const selected = scrollContainerElem?.querySelector('.selected-entry'); - selected?.scrollIntoView({block: 'center'}); + selected?.scrollIntoView({block: 'nearest'}); }); } @@ -108,8 +108,8 @@ {:else} selectEntry(entry)} noShadow class="!rounded-none" diff --git a/frontend/viewer/src/lib/layout/FieldListDialog.svelte b/frontend/viewer/src/lib/layout/FieldListDialog.svelte index 8c01a03c2..cf84f094d 100644 --- a/frontend/viewer/src/lib/layout/FieldListDialog.svelte +++ b/frontend/viewer/src/lib/layout/FieldListDialog.svelte @@ -32,7 +32,6 @@
diff --git a/frontend/viewer/src/lib/mini-lcm/complex-form-component.ts b/frontend/viewer/src/lib/mini-lcm/complex-form-component.ts new file mode 100644 index 000000000..6aee0e1a6 --- /dev/null +++ b/frontend/viewer/src/lib/mini-lcm/complex-form-component.ts @@ -0,0 +1,20 @@ +/** + * This is a TypeGen auto-generated file. + * Any changes made to this file can be lost when this file is regenerated. + */ + +import type { IComplexFormComponent } from "./i-complex-form-component"; + +export class ComplexFormComponent implements IComplexFormComponent { + constructor(id: string, complexFormEntryId: string, componentEntryId: string) { + this.id = id; + this.complexFormEntryId = complexFormEntryId; + this.componentEntryId = componentEntryId; + } + id: string; + complexFormEntryId: string; + complexFormHeadword?: string; + componentEntryId: string; + componentSenseId?: string; + componentHeadword?: string; +} diff --git a/frontend/viewer/src/lib/mini-lcm/entry.ts b/frontend/viewer/src/lib/mini-lcm/entry.ts index 695ea8367..c55ac0d8c 100644 --- a/frontend/viewer/src/lib/mini-lcm/entry.ts +++ b/frontend/viewer/src/lib/mini-lcm/entry.ts @@ -3,6 +3,8 @@ * Any changes made to this file can be lost when this file is regenerated. */ +import type { IComplexFormComponent } from './i-complex-form-component'; +import type { IComplexFormType } from './i-complex-form-type'; import {type IEntry} from './i-entry'; import {type IMultiString} from './i-multi-string'; import {type ISense} from './i-sense'; @@ -15,6 +17,9 @@ export class Entry implements IEntry { id: string; lexemeForm: IMultiString = {}; citationForm: IMultiString = {}; + complexForms: IComplexFormComponent[] = []; + complexFormTypes: IComplexFormType[] = []; + components: IComplexFormComponent[] = []; literalMeaning: IMultiString = {}; senses: ISense[] = []; note: IMultiString = {}; diff --git a/frontend/viewer/src/lib/mini-lcm/i-complex-form-component.ts b/frontend/viewer/src/lib/mini-lcm/i-complex-form-component.ts new file mode 100644 index 000000000..e766b7892 --- /dev/null +++ b/frontend/viewer/src/lib/mini-lcm/i-complex-form-component.ts @@ -0,0 +1,13 @@ +/** + * This is a TypeGen auto-generated file. + * Any changes made to this file can be lost when this file is regenerated. + */ + +export interface IComplexFormComponent { + id: string; + complexFormEntryId: string; + complexFormHeadword?: string; + componentEntryId: string; + componentSenseId?: string; + componentHeadword?: string; +} diff --git a/frontend/viewer/src/lib/mini-lcm/i-complex-form-type.ts b/frontend/viewer/src/lib/mini-lcm/i-complex-form-type.ts new file mode 100644 index 000000000..aef9493a1 --- /dev/null +++ b/frontend/viewer/src/lib/mini-lcm/i-complex-form-type.ts @@ -0,0 +1,6 @@ +import type { IMultiString } from "./i-multi-string"; + +export interface IComplexFormType { + id: string; + name: IMultiString; +} diff --git a/frontend/viewer/src/lib/mini-lcm/i-entry.ts b/frontend/viewer/src/lib/mini-lcm/i-entry.ts index 11a9a60cd..fc9ef513f 100644 --- a/frontend/viewer/src/lib/mini-lcm/i-entry.ts +++ b/frontend/viewer/src/lib/mini-lcm/i-entry.ts @@ -3,6 +3,8 @@ * Any changes made to this file can be lost when this file is regenerated. */ +import type { IComplexFormComponent } from './i-complex-form-component'; +import type { IComplexFormType } from './i-complex-form-type'; import { type IMultiString } from './i-multi-string'; import { type ISense } from './i-sense'; @@ -10,6 +12,9 @@ export interface IEntry { id: string; lexemeForm: IMultiString; citationForm: IMultiString; + complexForms: IComplexFormComponent[]; + complexFormTypes: IComplexFormType[]; + components: IComplexFormComponent[]; literalMeaning: IMultiString; senses: ISense[]; note: IMultiString; diff --git a/frontend/viewer/src/lib/mini-lcm/index.ts b/frontend/viewer/src/lib/mini-lcm/index.ts index c51efb880..a3d3f72e2 100644 --- a/frontend/viewer/src/lib/mini-lcm/index.ts +++ b/frontend/viewer/src/lib/mini-lcm/index.ts @@ -16,3 +16,6 @@ export * from './writing-system'; export * from './writing-systems'; export * from './part-of-speech'; export * from './semantic-domain'; +export * from './complex-form-component'; +export * from './i-complex-form-component'; +export * from './i-complex-form-type'; diff --git a/frontend/viewer/src/lib/notifications/NotificationOutlet.svelte b/frontend/viewer/src/lib/notifications/NotificationOutlet.svelte index 0f52085c8..46e0db57e 100644 --- a/frontend/viewer/src/lib/notifications/NotificationOutlet.svelte +++ b/frontend/viewer/src/lib/notifications/NotificationOutlet.svelte @@ -10,6 +10,7 @@ const notifications = AppNotification.notifications; +{#if $notifications.length}
{#each $notifications as notification}
@@ -35,3 +36,4 @@
{/each}
+{/if} diff --git a/frontend/viewer/src/lib/search-bar/SearchBar.svelte b/frontend/viewer/src/lib/search-bar/SearchBar.svelte index c50a2722a..77522811a 100644 --- a/frontend/viewer/src/lib/search-bar/SearchBar.svelte +++ b/frontend/viewer/src/lib/search-bar/SearchBar.svelte @@ -1,5 +1,5 @@