@museumwnf/viewer-core 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE.md +44 -0
  2. package/README.md +635 -0
  3. package/package.json +50 -0
  4. package/src/AppRoot.vue +58 -0
  5. package/src/catalogue/dates.js +209 -0
  6. package/src/catalogue/facets.js +61 -0
  7. package/src/catalogue/keywordIndex.js +289 -0
  8. package/src/catalogue/listQuery.js +100 -0
  9. package/src/catalogue/pagination.js +57 -0
  10. package/src/catalogue/searchLanguage.js +32 -0
  11. package/src/catalogue/timeline.js +220 -0
  12. package/src/composables/useCatalogueData.js +141 -0
  13. package/src/composables/useDataPackage.js +98 -0
  14. package/src/composables/useEntities.js +100 -0
  15. package/src/composables/useRecordLanguage.js +71 -0
  16. package/src/conventions.js +148 -0
  17. package/src/createViewer.js +29 -0
  18. package/src/i18n/I18nText.vue +17 -0
  19. package/src/i18n/I18nTextInline.vue +17 -0
  20. package/src/i18n/index.js +88 -0
  21. package/src/i18n/language.js +127 -0
  22. package/src/i18n/markdown.js +265 -0
  23. package/src/index.js +50 -0
  24. package/src/injectionKeys.js +1 -0
  25. package/src/languages.js +49 -0
  26. package/src/legacy/index.js +58 -0
  27. package/src/record/collectionTree.js +307 -0
  28. package/src/record/derivations.js +154 -0
  29. package/src/record/partners.js +87 -0
  30. package/src/record/recordSheet.js +231 -0
  31. package/src/rights.js +53 -0
  32. package/src/router/index.js +138 -0
  33. package/src/siteConfig.js +37 -0
  34. package/src/styles/base.css +49 -0
  35. package/src/testing/checkOfferedLanguages.js +67 -0
  36. package/src/testing/index.js +3 -0
  37. package/src/testing/smoke.js +103 -0
  38. package/src/testing/viteConfig.js +90 -0
  39. package/src/views/DetailView.vue +53 -0
  40. package/src/views/HomeView.vue +20 -0
  41. package/src/views/ListView.vue +61 -0
  42. package/src/views/NotFoundView.vue +8 -0
  43. package/src/vite.js +8 -0
@@ -0,0 +1,90 @@
1
+ import path from 'node:path'
2
+
3
+ /**
4
+ * The shared Vite configuration for viewer websites, returning a config object
5
+ * ready to pass to `defineConfig()`. The seven websites duplicate this shape:
6
+ * the `@inventory-data` alias to the data package, `optimizeDeps` to inline
7
+ * the viewer packages, `testTimeout`, and the test environment.
8
+ *
9
+ * `dataPackage` is the npm package name, e.g. `'@museumwnf/islamicart-data'`.
10
+ * `inline` is an optional array of extra packages to inline in tests, beyond
11
+ * the required `'@museumwnf/viewer-core'` and `'@museumwnf/viewer-layout'`
12
+ * (and, transitionally, their pre-#1722 `'@metanull/*'` spellings — see the
13
+ * comment on `test.server.deps.inline` below).
14
+ * `plugins` is an optional array of Vite plugins (e.g. `@vitejs/plugin-vue`),
15
+ * which the site must supply because this package does not depend on them.
16
+ * `root` is an optional project root path (default `process.cwd()`); use it when
17
+ * loading the config from a context other than the project directory.
18
+ */
19
+ export function defineViewerConfig({
20
+ dataPackage,
21
+ inline = [],
22
+ plugins = [],
23
+ root = process.cwd(),
24
+ } = {}) {
25
+ return {
26
+ plugins,
27
+ resolve: {
28
+ alias: {
29
+ // viewer-core reads every JSON of the data package through this alias.
30
+ // Resolve against the caller's project root, not this file's installed
31
+ // location: import.meta.url is the package's own path inside node_modules,
32
+ // not the site's project root where its node_modules actually exists.
33
+ '@inventory-data': path.join(root, 'node_modules', dataPackage),
34
+ },
35
+ },
36
+ optimizeDeps: {
37
+ // viewer-core ships .vue source that esbuild pre-bundling cannot parse;
38
+ // viewer-layout must not be pre-bundled either or its chunk gets a second
39
+ // copy of the Vue runtime in dev (both packages share the app's vue).
40
+ // The /i18n subpath is listed as well as the package: Vite pre-bundles a
41
+ // subpath as its own entry, and a second copy of the text module would be
42
+ // a second, empty set of texts for whatever imported it.
43
+ //
44
+ // Both the @museumwnf and @metanull spellings are listed: this list is
45
+ // matched against the *site's own* import specifier, which still says
46
+ // @metanull/* until every site completes its own move in
47
+ // metanull/inventory-app#1722. Matching only the new name silently stops
48
+ // excluding the package for every current consumer.
49
+ exclude: [
50
+ '@museumwnf/viewer-core',
51
+ '@museumwnf/viewer-core/i18n',
52
+ '@museumwnf/viewer-layout',
53
+ '@metanull/viewer-core',
54
+ '@metanull/viewer-core/i18n',
55
+ '@metanull/viewer-layout',
56
+ ],
57
+ // The runtime deps reach the browser through those excluded packages, so
58
+ // the dev-server dependency scan cannot discover them until the website's
59
+ // own views import them directly. Without this list a late discovery
60
+ // pre-bundles a second copy of Vue next to the raw one already loaded,
61
+ // and the dev server crashes on boot. Listing them pre-bundles each
62
+ // exactly once, and the excluded packages get the same copy.
63
+ include: ['vue', 'vue-router'],
64
+ },
65
+ test: {
66
+ environment: 'jsdom',
67
+ testTimeout: 60000,
68
+ server: {
69
+ deps: {
70
+ // viewer-core ships .vue source; Node cannot load it unless Vitest
71
+ // processes the package instead of externalizing it, matched by the
72
+ // specifier the *importing site* actually wrote. viewer-layout's
73
+ // composed views import viewer-core, so the layout is processed too.
74
+ // Both scopes are listed for the same transitional reason as
75
+ // optimizeDeps.exclude above: sites still import @metanull/* until
76
+ // metanull/inventory-app#1722, so matching only @museumwnf/* would
77
+ // externalize this package for every current consumer, sending its
78
+ // raw .vue files to Node's loader instead of Vitest's transform.
79
+ inline: [
80
+ '@museumwnf/viewer-core',
81
+ '@museumwnf/viewer-layout',
82
+ '@metanull/viewer-core',
83
+ '@metanull/viewer-layout',
84
+ ...inline,
85
+ ],
86
+ },
87
+ },
88
+ },
89
+ }
90
+ }
@@ -0,0 +1,53 @@
1
+ <script setup>
2
+ import { computed, ref, watchEffect } from 'vue'
3
+ import { useDataPackage } from '../composables/useDataPackage.js'
4
+ import { renderBlock } from '../i18n/markdown.js'
5
+
6
+ const props = defineProps({
7
+ entity: { type: String, required: true },
8
+ id: { type: String, required: true },
9
+ })
10
+
11
+ const { loadEntity } = useDataPackage()
12
+
13
+ const record = ref(undefined)
14
+ watchEffect(async () => {
15
+ // Read both props before the first await so the effect tracks them.
16
+ const { entity, id } = props
17
+ const all = await loadEntity(entity)
18
+ record.value = all.find((r) => String(r.id) === String(id)) ?? null
19
+ })
20
+
21
+ const fields = computed(() =>
22
+ record.value ? Object.entries(record.value).filter(([key]) => key !== 'id') : []
23
+ )
24
+
25
+ // Through the same pipeline as every text, which escapes raw HTML rather than
26
+ // rendering it. A data package holds Markdown: the importer converts the legacy
27
+ // HTML on the way in, so HTML reaching a field here is a fault upstream and the
28
+ // fix belongs in the importer. Rendering it would hide that, and would make a
29
+ // museum record — written by hand, years ago, in another system — the one input
30
+ // this application trusts with markup.
31
+ function renderValue(value) {
32
+ return typeof value === 'string' ? renderBlock(value, { breaks: true }) : JSON.stringify(value)
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <article class="vc-detail">
38
+ <p v-if="record === undefined">{{ $t('core.status.loading') }}</p>
39
+ <p v-else-if="record === null">{{ $t('core.detail.notFound') }}</p>
40
+ <template v-else>
41
+ <h1>{{ record.title ?? record.name ?? record.id }}</h1>
42
+ <dl>
43
+ <template v-for="[key, value] in fields" :key="key">
44
+ <dt>{{ key }}</dt>
45
+ <dd v-html="renderValue(value)"></dd>
46
+ </template>
47
+ </dl>
48
+ </template>
49
+ <p>
50
+ <router-link :to="`/${entity}`">{{ $t('core.nav.backToList') }}</router-link>
51
+ </p>
52
+ </article>
53
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup>
2
+ import { inject } from 'vue'
3
+ import { VIEWER_CONFIG } from '../injectionKeys.js'
4
+
5
+ const config = inject(VIEWER_CONFIG, {})
6
+ const entities = config.features?.entities ?? []
7
+ </script>
8
+
9
+ <template>
10
+ <section class="vc-home">
11
+ <h1>{{ config.siteName }}</h1>
12
+ <nav v-if="entities.length">
13
+ <ul>
14
+ <li v-for="entity in entities" :key="entity">
15
+ <router-link :to="`/${entity}`">{{ entity }}</router-link>
16
+ </li>
17
+ </ul>
18
+ </nav>
19
+ </section>
20
+ </template>
@@ -0,0 +1,61 @@
1
+ <script setup>
2
+ import { computed, ref, watchEffect } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+ import { useDataPackage } from '../composables/useDataPackage.js'
5
+ import { paginate } from '../catalogue/pagination.js'
6
+
7
+ const props = defineProps({
8
+ entity: { type: String, required: true },
9
+ pageSize: { type: Number, default: 20 },
10
+ })
11
+
12
+ const route = useRoute()
13
+ const { loadEntity } = useDataPackage()
14
+
15
+ const records = ref(null)
16
+ watchEffect(async () => {
17
+ records.value = await loadEntity(props.entity)
18
+ })
19
+
20
+ // The same page arithmetic every website's lists use, so this generic list
21
+ // and a site's results page cannot disagree on where page 3 starts.
22
+ const paged = computed(() => paginate(records.value ?? [], route.query.page, props.pageSize))
23
+ const page = computed(() => paged.value.currentPage)
24
+ const totalPages = computed(() => paged.value.lastPage)
25
+ const pageRecords = computed(() => paged.value.rows)
26
+
27
+ function labelOf(record) {
28
+ return record.title ?? record.name ?? record.label ?? record.id
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <section class="vc-list">
34
+ <h1>{{ entity }}</h1>
35
+ <p v-if="records === null">{{ $t('core.status.loading') }}</p>
36
+ <p v-else-if="records.length === 0">{{ $t('core.list.empty') }}</p>
37
+ <template v-else>
38
+ <ul>
39
+ <li v-for="record in pageRecords" :key="record.id">
40
+ <router-link :to="`/${entity}/${record.id}`">{{ labelOf(record) }}</router-link>
41
+ </li>
42
+ </ul>
43
+ <nav v-if="totalPages > 1" class="vc-pagination">
44
+ <router-link v-if="page > 1" :to="{ query: { page: page - 1 } }">
45
+ {{ $t('core.pagination.previous') }}
46
+ </router-link>
47
+ <!-- The position is rendered here, next to the texts, rather than
48
+ inserted into one: a text that has to carry a number cannot be
49
+ translated without carrying the number's place in the sentence
50
+ too, and that is the one thing this system does not do. -->
51
+ <span class="vc-pagination__position">{{ page }} / {{ totalPages }}</span>
52
+ <router-link v-if="page < totalPages" :to="{ query: { page: page + 1 } }">
53
+ {{ $t('core.pagination.next') }}
54
+ </router-link>
55
+ </nav>
56
+ </template>
57
+ <p>
58
+ <router-link to="/">{{ $t('core.nav.home') }}</router-link>
59
+ </p>
60
+ </section>
61
+ </template>
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <section class="vc-not-found">
3
+ <p>{{ $t('core.notFound.page') }}</p>
4
+ <p>
5
+ <router-link to="/">{{ $t('core.nav.home') }}</router-link>
6
+ </p>
7
+ </section>
8
+ </template>
package/src/vite.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Vite configuration helper for viewer websites.
3
+ * This is a separate entry point because Vite's config loader runs under Node.js,
4
+ * which cannot parse .vue files. The ./testing barrel re-exports mountSite and
5
+ * other test utilities that import createViewer → AppRoot.vue, making it unsafe
6
+ * to import from a vite.config.js. This entry avoids that chain.
7
+ */
8
+ export { defineViewerConfig } from './testing/viteConfig.js'