relaton 3.0.7 → 3.1.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.
- checksums.yaml +4 -4
- data/bin/relaton +20 -0
- data/lib/relaton/3gpp/processor.rb +1 -1
- data/lib/relaton/3gpp.rb +3 -3
- data/lib/relaton/adobe/processor.rb +1 -1
- data/lib/relaton/adobe.rb +3 -3
- data/lib/relaton/bib/item_data.rb +2 -2
- data/lib/relaton/bib/model/docidentifier.rb +17 -2
- data/lib/relaton/bib.rb +2 -2
- data/lib/relaton/bibcollection.rb +146 -0
- data/lib/relaton/bibdata.rb +92 -0
- data/lib/relaton/bipm/data_outcomes_parser.rb +6 -1
- data/lib/relaton/bipm/processor.rb +1 -1
- data/lib/relaton/bipm.rb +3 -3
- data/lib/relaton/bsi/processor.rb +1 -1
- data/lib/relaton/bsi.rb +1 -1
- data/lib/relaton/calconnect/data_fetcher.rb +2 -2
- data/lib/relaton/calconnect/model/item.rb +1 -1
- data/lib/relaton/calconnect/processor.rb +1 -1
- data/lib/relaton/calconnect.rb +2 -2
- data/lib/relaton/ccsds/data/fetcher.rb +1 -1
- data/lib/relaton/ccsds/model/item.rb +1 -1
- data/lib/relaton/ccsds/processor.rb +1 -1
- data/lib/relaton/ccsds/util.rb +1 -1
- data/lib/relaton/ccsds.rb +1 -1
- data/lib/relaton/cen/processor.rb +1 -1
- data/lib/relaton/cen.rb +2 -2
- data/lib/relaton/cie/data_fetcher.rb +3 -3
- data/lib/relaton/cie/processor.rb +1 -1
- data/lib/relaton/cie.rb +2 -2
- data/lib/relaton/cli/base_convertor.rb +134 -0
- data/lib/relaton/cli/command.rb +396 -0
- data/lib/relaton/cli/data_fetcher.rb +20 -0
- data/lib/relaton/cli/frontend/dist/404.html +18 -0
- data/lib/relaton/cli/frontend/dist/app.iife.js +1 -0
- data/lib/relaton/cli/frontend/dist/style.css +3 -0
- data/lib/relaton/cli/frontend/index.html +14 -0
- data/lib/relaton/cli/frontend/package-lock.json +2945 -0
- data/lib/relaton/cli/frontend/package.json +29 -0
- data/lib/relaton/cli/frontend/public/404.html +18 -0
- data/lib/relaton/cli/frontend/src/App.test.ts +501 -0
- data/lib/relaton/cli/frontend/src/App.vue +506 -0
- data/lib/relaton/cli/frontend/src/app.ts +90 -0
- data/lib/relaton/cli/frontend/src/components/DocumentDetail.test.ts +142 -0
- data/lib/relaton/cli/frontend/src/components/DocumentDetail.vue +228 -0
- data/lib/relaton/cli/frontend/src/components/DocumentRow.vue +88 -0
- data/lib/relaton/cli/frontend/src/components/Icon.test.ts +44 -0
- data/lib/relaton/cli/frontend/src/components/Icon.vue +55 -0
- data/lib/relaton/cli/frontend/src/components/Pagination.vue +26 -0
- data/lib/relaton/cli/frontend/src/dev/main.ts +100 -0
- data/lib/relaton/cli/frontend/src/dev/sample-data.json +71 -0
- data/lib/relaton/cli/frontend/src/lib/filter.test.ts +88 -0
- data/lib/relaton/cli/frontend/src/lib/filter.ts +95 -0
- data/lib/relaton/cli/frontend/src/lib/hydrate.test.ts +102 -0
- data/lib/relaton/cli/frontend/src/lib/hydrate.ts +55 -0
- data/lib/relaton/cli/frontend/src/lib/shards.test.ts +319 -0
- data/lib/relaton/cli/frontend/src/lib/shards.ts +183 -0
- data/lib/relaton/cli/frontend/src/lib/types.ts +148 -0
- data/lib/relaton/cli/frontend/src/lib/url.test.ts +151 -0
- data/lib/relaton/cli/frontend/src/lib/url.ts +111 -0
- data/lib/relaton/cli/frontend/src/shims-vue.d.ts +5 -0
- data/lib/relaton/cli/frontend/src/styles/index.css +27 -0
- data/lib/relaton/cli/frontend/src/test/fetch.ts +58 -0
- data/lib/relaton/cli/frontend/tsconfig.json +20 -0
- data/lib/relaton/cli/frontend/vite.config.ts +42 -0
- data/lib/relaton/cli/frontend/vitest.config.ts +11 -0
- data/lib/relaton/cli/frontend_assets.rb +58 -0
- data/lib/relaton/cli/full_text_search.rb +98 -0
- data/lib/relaton/cli/i18n.yaml +1 -0
- data/lib/relaton/cli/index_item_normalizer.rb +380 -0
- data/lib/relaton/cli/index_site_generator.rb +1043 -0
- data/lib/relaton/cli/relaton_file.rb +290 -0
- data/lib/relaton/cli/subcommand_collection.rb +263 -0
- data/lib/relaton/cli/subcommand_db.rb +81 -0
- data/lib/relaton/cli/templates/_document.liquid +111 -0
- data/lib/relaton/cli/templates/_index.liquid +61 -0
- data/lib/relaton/cli/templates/index/page.liquid +20 -0
- data/lib/relaton/cli/templates/index-style.css +1017 -0
- data/lib/relaton/cli/util.rb +8 -0
- data/lib/relaton/cli/xml_convertor.rb +47 -0
- data/lib/relaton/cli/xml_to_html_renderer.rb +117 -0
- data/lib/relaton/cli/yaml_convertor.rb +102 -0
- data/lib/relaton/cli.rb +103 -0
- data/lib/relaton/core/processor.rb +1 -1
- data/lib/relaton/core.rb +1 -1
- data/lib/relaton/db/registry.rb +1 -1
- data/lib/relaton/db.rb +1 -1
- data/lib/relaton/doi/CLAUDE.md +1 -1
- data/lib/relaton/doi/parser.rb +2 -1
- data/lib/relaton/doi/processor.rb +1 -1
- data/lib/relaton/doi.rb +4 -4
- data/lib/relaton/easc/processor.rb +1 -1
- data/lib/relaton/easc.rb +3 -3
- data/lib/relaton/ecma/data_fetcher.rb +1 -1
- data/lib/relaton/ecma/processor.rb +1 -1
- data/lib/relaton/ecma.rb +2 -2
- data/lib/relaton/element_finder.rb +29 -0
- data/lib/relaton/etsi/data_fetcher.rb +2 -2
- data/lib/relaton/etsi/item.rb +1 -1
- data/lib/relaton/etsi/processor.rb +2 -2
- data/lib/relaton/etsi.rb +1 -1
- data/lib/relaton/evs/processor.rb +1 -1
- data/lib/relaton/evs.rb +2 -2
- data/lib/relaton/gb/CLAUDE.md +1 -1
- data/lib/relaton/gb/bibliography.rb +1 -1
- data/lib/relaton/gb/processor.rb +1 -1
- data/lib/relaton/gb.rb +2 -2
- data/lib/relaton/gost/processor.rb +1 -1
- data/lib/relaton/gost.rb +3 -3
- data/lib/relaton/iala/processor.rb +1 -1
- data/lib/relaton/iala.rb +3 -3
- data/lib/relaton/iana/data_fetcher.rb +1 -1
- data/lib/relaton/iana/processor.rb +1 -1
- data/lib/relaton/iana.rb +2 -2
- data/lib/relaton/iec/data_fetcher.rb +1 -1
- data/lib/relaton/iec/processor.rb +1 -1
- data/lib/relaton/iec.rb +2 -2
- data/lib/relaton/ieee/processor.rb +1 -1
- data/lib/relaton/ieee.rb +2 -2
- data/lib/relaton/ietf/CLAUDE.md +1 -1
- data/lib/relaton/ietf/data_fetcher.rb +1 -1
- data/lib/relaton/ietf/processor.rb +1 -1
- data/lib/relaton/ietf.rb +2 -2
- data/lib/relaton/iho/CLAUDE.md +3 -3
- data/lib/relaton/iho/bibliography.rb +41 -27
- data/lib/relaton/iho/hash_parser_v1.rb +1 -1
- data/lib/relaton/iho/processor.rb +4 -3
- data/lib/relaton/iho.rb +2 -2
- data/lib/relaton/index.rb +1 -1
- data/lib/relaton/isbn/processor.rb +1 -1
- data/lib/relaton/isbn.rb +1 -1
- data/lib/relaton/iso/hash_parser_v1.rb +1 -1
- data/lib/relaton/iso/processor.rb +1 -1
- data/lib/relaton/iso.rb +3 -3
- data/lib/relaton/itu/processor.rb +1 -1
- data/lib/relaton/itu.rb +3 -3
- data/lib/relaton/jcgm/processor.rb +1 -1
- data/lib/relaton/jcgm.rb +2 -2
- data/lib/relaton/jis/processor.rb +1 -1
- data/lib/relaton/jis.rb +2 -2
- data/lib/relaton/nist/CLAUDE.md +11 -1
- data/lib/relaton/nist/README.adoc +7 -1
- data/lib/relaton/nist/hit_collection.rb +25 -1
- data/lib/relaton/nist/mods_parser.rb +32 -8
- data/lib/relaton/nist/processor.rb +1 -1
- data/lib/relaton/nist/pubs_export.rb +69 -11
- data/lib/relaton/nist.rb +2 -2
- data/lib/relaton/oasis/processor.rb +1 -1
- data/lib/relaton/oasis.rb +2 -2
- data/lib/relaton/ogc/CLAUDE.md +1 -1
- data/lib/relaton/ogc/processor.rb +1 -1
- data/lib/relaton/ogc.rb +2 -2
- data/lib/relaton/oiml/processor.rb +1 -1
- data/lib/relaton/oiml.rb +2 -2
- data/lib/relaton/omg/CLAUDE.md +1 -1
- data/lib/relaton/omg/processor.rb +1 -1
- data/lib/relaton/omg.rb +1 -1
- data/lib/relaton/plateau/CLAUDE.md +1 -1
- data/lib/relaton/plateau/data_fetcher.rb +1 -1
- data/lib/relaton/plateau/processor.rb +1 -1
- data/lib/relaton/plateau.rb +2 -2
- data/lib/relaton/render/general.rb +319 -0
- data/lib/relaton/render/iso690/csl_import.rb +129 -0
- data/lib/relaton/render/iso690/element.rb +153 -0
- data/lib/relaton/render/iso690/elements/access.rb +44 -0
- data/lib/relaton/render/iso690/elements/authorizer.rb +45 -0
- data/lib/relaton/render/iso690/elements/citeid.rb +39 -0
- data/lib/relaton/render/iso690/elements/component_part.rb +97 -0
- data/lib/relaton/render/iso690/elements/creator.rb +267 -0
- data/lib/relaton/render/iso690/elements/edition.rb +97 -0
- data/lib/relaton/render/iso690/elements/extent.rb +71 -0
- data/lib/relaton/render/iso690/elements/identifier.rb +46 -0
- data/lib/relaton/render/iso690/elements/location.rb +73 -0
- data/lib/relaton/render/iso690/elements/medium.rb +32 -0
- data/lib/relaton/render/iso690/elements/numeration.rb +26 -0
- data/lib/relaton/render/iso690/elements/production.rb +111 -0
- data/lib/relaton/render/iso690/elements/pub_date.rb +63 -0
- data/lib/relaton/render/iso690/elements/series.rb +66 -0
- data/lib/relaton/render/iso690/elements/size.rb +45 -0
- data/lib/relaton/render/iso690/elements/status.rb +30 -0
- data/lib/relaton/render/iso690/elements/stddoc.rb +40 -0
- data/lib/relaton/render/iso690/elements/title.rb +56 -0
- data/lib/relaton/render/iso690/elements/updated.rb +38 -0
- data/lib/relaton/render/iso690/elements.rb +67 -0
- data/lib/relaton/render/iso690/fields.rb +142 -0
- data/lib/relaton/render/iso690/i18n/en.yml +48 -0
- data/lib/relaton/render/iso690/i18n/fr.yml +14 -0
- data/lib/relaton/render/iso690/i18n/ja.yml +38 -0
- data/lib/relaton/render/iso690/i18n.rb +83 -0
- data/lib/relaton/render/iso690/identifier_kinds.rb +29 -0
- data/lib/relaton/render/iso690/index_renderer.rb +57 -0
- data/lib/relaton/render/iso690/kinds.rb +53 -0
- data/lib/relaton/render/iso690/renderer.rb +150 -0
- data/lib/relaton/render/iso690/rules/bipm.rb +186 -0
- data/lib/relaton/render/iso690/rules/jis.rb +103 -0
- data/lib/relaton/render/iso690/rules/nist.rb +384 -0
- data/lib/relaton/render/iso690/rules.rb +227 -0
- data/lib/relaton/render/iso690/style/locale.rb +59 -0
- data/lib/relaton/render/iso690/style/name_form.rb +87 -0
- data/lib/relaton/render/iso690/style/scheme.rb +33 -0
- data/lib/relaton/render/iso690/style/sort_rule.rb +25 -0
- data/lib/relaton/render/iso690/style/template_map.rb +40 -0
- data/lib/relaton/render/iso690/style/type_template.rb +45 -0
- data/lib/relaton/render/iso690/style.rb +227 -0
- data/lib/relaton/render/iso690/styles/apa.yml +45 -0
- data/lib/relaton/render/iso690/styles/author-date.yml +31 -0
- data/lib/relaton/render/iso690/styles/chicago.yml +49 -0
- data/lib/relaton/render/iso690/styles/ieee-sa.yml +45 -0
- data/lib/relaton/render/iso690/styles/jis-en.yml +68 -0
- data/lib/relaton/render/iso690/styles/jis-ja.yml +67 -0
- data/lib/relaton/render/iso690/styles/lncs.yml +54 -0
- data/lib/relaton/render/iso690/styles/mla.yml +56 -0
- data/lib/relaton/render/iso690/template.rb +248 -0
- data/lib/relaton/render/iso690.rb +28 -0
- data/lib/relaton/sae/CLAUDE.md +89 -0
- data/lib/relaton/sae/bibdata.rb +8 -0
- data/lib/relaton/sae/bibitem.rb +8 -0
- data/lib/relaton/sae/bibliography.rb +128 -0
- data/lib/relaton/sae/data_fetcher.rb +219 -0
- data/lib/relaton/sae/doi_code.rb +83 -0
- data/lib/relaton/sae/item.rb +10 -0
- data/lib/relaton/sae/item_data.rb +6 -0
- data/lib/relaton/sae/processor.rb +62 -0
- data/lib/relaton/sae/util.rb +9 -0
- data/lib/relaton/sae.rb +37 -0
- data/lib/relaton/un/CLAUDE.md +1 -1
- data/lib/relaton/un/processor.rb +1 -1
- data/lib/relaton/un.rb +1 -1
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/bibliography.rb +1 -1
- data/lib/relaton/w3c/data_fetcher.rb +1 -1
- data/lib/relaton/w3c/processor.rb +1 -1
- data/lib/relaton/w3c.rb +2 -2
- data/lib/relaton/xsf/data_fetcher.rb +1 -1
- data/lib/relaton/xsf/processor.rb +1 -1
- data/lib/relaton/xsf.rb +2 -2
- data/lib/relaton-cli.rb +17 -0
- data/lib/relaton-render.rb +12 -0
- data/lib/relaton.rb +3 -2
- metadata +203 -23
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { mount } from "@vue/test-utils";
|
|
4
|
+
import DocumentDetail from "./DocumentDetail.vue";
|
|
5
|
+
import type { IndexDocument } from "../lib/types";
|
|
6
|
+
|
|
7
|
+
const rich: IndexDocument = {
|
|
8
|
+
id: "ISO 1234:2020",
|
|
9
|
+
title: "A standard about things",
|
|
10
|
+
doctype: "international-standard",
|
|
11
|
+
stage: "published",
|
|
12
|
+
date: "2020-05-01",
|
|
13
|
+
link: "https://www.iso.org/standard/1234.html",
|
|
14
|
+
yaml: "https://raw.example/data/iso-1234.yaml",
|
|
15
|
+
abstract: "This document specifies the things.",
|
|
16
|
+
edition: "2",
|
|
17
|
+
languages: ["en", "fr"],
|
|
18
|
+
keywords: ["metrology", "units"],
|
|
19
|
+
publisher: "International Organization for Standardization",
|
|
20
|
+
contributors: [
|
|
21
|
+
{ name: "International Organization for Standardization", role: "publisher" },
|
|
22
|
+
{ name: "Ada Lovelace", role: "author" },
|
|
23
|
+
],
|
|
24
|
+
docids: [
|
|
25
|
+
{ id: "ISO 1234:2020", type: "ISO", language: "en" },
|
|
26
|
+
{ id: "10.1000/xyz", type: "DOI" },
|
|
27
|
+
],
|
|
28
|
+
dates: [
|
|
29
|
+
{ type: "published", value: "2020-05-01" },
|
|
30
|
+
{ type: "created", value: "2019" },
|
|
31
|
+
],
|
|
32
|
+
relations: [
|
|
33
|
+
{ type: "obsoletes", id: "ISO 1234:2015" },
|
|
34
|
+
{ type: "obsoletedBy", id: "ISO/IEC/IEEE 60559:2011" },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// "ISO 1234:2015" is in this dataset; the ISO/IEC/IEEE one is not.
|
|
39
|
+
const hasDoc = (id: string) => id === "ISO 1234:2015";
|
|
40
|
+
|
|
41
|
+
const minimal: IndexDocument = {
|
|
42
|
+
id: "RAW 9",
|
|
43
|
+
title: "Just a title",
|
|
44
|
+
doctype: null,
|
|
45
|
+
stage: null,
|
|
46
|
+
date: null,
|
|
47
|
+
link: null,
|
|
48
|
+
yaml: null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe("DocumentDetail", () => {
|
|
52
|
+
it("renders the id, title and all enriched fields", () => {
|
|
53
|
+
const w = mount(DocumentDetail, { props: { doc: rich } });
|
|
54
|
+
const text = w.text();
|
|
55
|
+
expect(text).toContain("ISO 1234:2020");
|
|
56
|
+
expect(text).toContain("A standard about things");
|
|
57
|
+
expect(text).toContain("This document specifies the things.");
|
|
58
|
+
expect(text).toContain("Ada Lovelace");
|
|
59
|
+
expect(text).toContain("author");
|
|
60
|
+
expect(text).toContain("International Organization for Standardization");
|
|
61
|
+
expect(text).toContain("10.1000/xyz");
|
|
62
|
+
expect(text).toContain("metrology");
|
|
63
|
+
expect(text).toContain("2019");
|
|
64
|
+
expect(text).toContain("published");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("links to the landing page and the raw YAML source", () => {
|
|
68
|
+
const w = mount(DocumentDetail, { props: { doc: rich } });
|
|
69
|
+
const hrefs = w.findAll("a").map((a) => a.attributes("href"));
|
|
70
|
+
expect(hrefs).toContain("https://www.iso.org/standard/1234.html");
|
|
71
|
+
expect(hrefs).toContain("https://raw.example/data/iso-1234.yaml");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("emits back when the Back control is clicked", async () => {
|
|
75
|
+
const w = mount(DocumentDetail, { props: { doc: rich } });
|
|
76
|
+
await w.get('[aria-label="Back to index"]').trigger("click");
|
|
77
|
+
expect(w.emitted("back")).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("degrades gracefully when only summary fields are present", () => {
|
|
81
|
+
const w = mount(DocumentDetail, { props: { doc: minimal } });
|
|
82
|
+
const text = w.text();
|
|
83
|
+
expect(text).toContain("RAW 9");
|
|
84
|
+
expect(text).toContain("Just a title");
|
|
85
|
+
// No enriched sections leak in.
|
|
86
|
+
expect(text).not.toContain("Abstract");
|
|
87
|
+
expect(text).not.toContain("Contributors");
|
|
88
|
+
expect(text).not.toContain("Identifiers");
|
|
89
|
+
expect(text).not.toContain("Relations");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("relations", () => {
|
|
93
|
+
it("lists every relation with a humanized type label", () => {
|
|
94
|
+
const w = mount(DocumentDetail, { props: { doc: rich, hasDoc } });
|
|
95
|
+
const text = w.text();
|
|
96
|
+
expect(text).toContain("Relations");
|
|
97
|
+
expect(text).toContain("Obsoletes");
|
|
98
|
+
expect(text).toContain("Obsoleted by");
|
|
99
|
+
expect(text).toContain("ISO 1234:2015");
|
|
100
|
+
expect(text).toContain("ISO/IEC/IEEE 60559:2011");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("links a target that is in the dataset to its ?doc= page", () => {
|
|
104
|
+
const w = mount(DocumentDetail, { props: { doc: rich, hasDoc } });
|
|
105
|
+
const hrefs = w.findAll("a").map((a) => a.attributes("href"));
|
|
106
|
+
expect(hrefs).toContain("/doc/ISO%201234%3A2015");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders a target outside the dataset as plain text, not a link", () => {
|
|
110
|
+
const w = mount(DocumentDetail, { props: { doc: rich, hasDoc } });
|
|
111
|
+
const linked = w.findAll("a").map((a) => a.text());
|
|
112
|
+
expect(linked).not.toContain("ISO/IEC/IEEE 60559:2011");
|
|
113
|
+
// …but it is still shown.
|
|
114
|
+
expect(w.text()).toContain("ISO/IEC/IEEE 60559:2011");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("emits open with the target id when a linked relation is clicked", async () => {
|
|
118
|
+
const w = mount(DocumentDetail, { props: { doc: rich, hasDoc } });
|
|
119
|
+
const link = w.findAll("a").find((a) => a.text() === "ISO 1234:2015");
|
|
120
|
+
await link!.trigger("click");
|
|
121
|
+
expect(w.emitted("open")).toEqual([["ISO 1234:2015"]]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// The anchor is a real ?doc= href precisely so it can be opened in a new
|
|
125
|
+
// tab; swallowing the modified click would break that.
|
|
126
|
+
it("lets a Cmd/Ctrl-click through to the browser instead of SPA-navigating", async () => {
|
|
127
|
+
const w = mount(DocumentDetail, { props: { doc: rich, hasDoc } });
|
|
128
|
+
const link = w.findAll("a").find((a) => a.text() === "ISO 1234:2015");
|
|
129
|
+
await link!.trigger("click", { metaKey: true });
|
|
130
|
+
expect(w.emitted("open")).toBeFalsy();
|
|
131
|
+
await link!.trigger("click", { ctrlKey: true });
|
|
132
|
+
expect(w.emitted("open")).toBeFalsy();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("leaves every relation unlinked when no resolver is supplied", () => {
|
|
136
|
+
const w = mount(DocumentDetail, { props: { doc: rich } });
|
|
137
|
+
const linked = w.findAll("a").map((a) => a.text());
|
|
138
|
+
expect(linked).not.toContain("ISO 1234:2015");
|
|
139
|
+
expect(w.text()).toContain("ISO 1234:2015");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import type { IndexDocument } from "../lib/types";
|
|
4
|
+
import { docHref } from "../lib/url";
|
|
5
|
+
import Icon from "./Icon.vue";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
doc: IndexDocument;
|
|
9
|
+
// Does this DocID exist in the current dataset? Deliberately a predicate
|
|
10
|
+
// rather than a Set: App builds that set from the whole corpus, which is
|
|
11
|
+
// expensive at 166k documents, and passing the built value would materialize
|
|
12
|
+
// it for every detail panel — including the majority that have no relations.
|
|
13
|
+
// A function stays lazy while still registering the reactive dependency, so
|
|
14
|
+
// unresolved targets upgrade from plain text to links as summary shards land.
|
|
15
|
+
hasDoc?: (id: string) => boolean;
|
|
16
|
+
}>();
|
|
17
|
+
defineEmits<{ back: []; open: [id: string] }>();
|
|
18
|
+
|
|
19
|
+
const copied = ref(false);
|
|
20
|
+
async function copyId() {
|
|
21
|
+
try {
|
|
22
|
+
await navigator.clipboard.writeText(props.doc.id);
|
|
23
|
+
copied.value = true;
|
|
24
|
+
setTimeout(() => (copied.value = false), 1200);
|
|
25
|
+
} catch {
|
|
26
|
+
/* clipboard may be blocked; ignore */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Each optional block renders only when it has content, so a summary-only
|
|
31
|
+
// document (dom / static-json modes, which don't carry the detail fields) shows
|
|
32
|
+
// just the header, core metadata and links — nothing empty leaks in.
|
|
33
|
+
const languages = computed(() => (props.doc.languages ?? []).join(", "));
|
|
34
|
+
const keywords = computed(() => props.doc.keywords ?? []);
|
|
35
|
+
const contributors = computed(() => props.doc.contributors ?? []);
|
|
36
|
+
const docids = computed(() => props.doc.docids ?? []);
|
|
37
|
+
const dates = computed(() => props.doc.dates ?? []);
|
|
38
|
+
// The single summary date is only worth showing when the full date list is
|
|
39
|
+
// absent (e.g. a summary-only build), to avoid duplicating the Dates section.
|
|
40
|
+
const showSummaryDate = computed(() => !!props.doc.date && dates.value.length === 0);
|
|
41
|
+
const hasMeta = computed(
|
|
42
|
+
() =>
|
|
43
|
+
!!(
|
|
44
|
+
props.doc.doctype ||
|
|
45
|
+
props.doc.stage ||
|
|
46
|
+
props.doc.edition ||
|
|
47
|
+
languages.value ||
|
|
48
|
+
props.doc.publisher ||
|
|
49
|
+
showSummaryDate.value
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
function docidLabel(d: { type?: string | null; language?: string | null }): string {
|
|
54
|
+
return [d.type, d.language].filter(Boolean).join(" · ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Relations, each paired with whether its target is a document in this dataset.
|
|
58
|
+
// A target that isn't (it may live in another relaton-data repo) is still listed
|
|
59
|
+
// — just as plain text, so nothing links to a page that doesn't exist here.
|
|
60
|
+
const relations = computed(() =>
|
|
61
|
+
(props.doc.relations ?? []).map((r) => ({
|
|
62
|
+
...r,
|
|
63
|
+
label: relationLabel(r.type),
|
|
64
|
+
href: props.hasDoc?.(r.id) ? docHref(r.id) : null,
|
|
65
|
+
})),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// "obsoletedBy" -> "Obsoleted by"; "partOf" -> "Part of".
|
|
69
|
+
function relationLabel(type?: string | null): string {
|
|
70
|
+
if (!type) return "Related";
|
|
71
|
+
const words = type.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
72
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<article class="py-2">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="mb-6 inline-flex items-center gap-1.5 text-sm text-brand hover:underline dark:text-brand-dark"
|
|
81
|
+
aria-label="Back to index"
|
|
82
|
+
@click="$emit('back')"
|
|
83
|
+
>
|
|
84
|
+
<Icon name="arrow-left" class="h-4 w-4" />Back to index
|
|
85
|
+
</button>
|
|
86
|
+
|
|
87
|
+
<header class="mb-6">
|
|
88
|
+
<div class="flex items-center gap-2">
|
|
89
|
+
<h2 class="font-mono text-lg font-semibold text-brand dark:text-brand-dark">
|
|
90
|
+
{{ doc.id }}
|
|
91
|
+
</h2>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
class="rounded p-0.5 text-slate-400 transition hover:text-brand"
|
|
95
|
+
:aria-label="`Copy DocID ${doc.id}`"
|
|
96
|
+
:title="copied ? 'Copied!' : 'Copy DocID'"
|
|
97
|
+
@click="copyId"
|
|
98
|
+
>
|
|
99
|
+
<Icon :name="copied ? 'check' : 'copy'" class="h-4 w-4" />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
<h1 class="mt-1 text-2xl font-semibold tracking-tight">{{ doc.title }}</h1>
|
|
103
|
+
</header>
|
|
104
|
+
|
|
105
|
+
<section v-if="doc.abstract" class="mb-6">
|
|
106
|
+
<h3 class="mb-1 text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
107
|
+
Abstract
|
|
108
|
+
</h3>
|
|
109
|
+
<p class="text-sm leading-relaxed text-slate-700 dark:text-slate-300">{{ doc.abstract }}</p>
|
|
110
|
+
</section>
|
|
111
|
+
|
|
112
|
+
<section v-if="hasMeta" class="mb-6">
|
|
113
|
+
<dl class="grid grid-cols-1 gap-x-6 gap-y-2 text-sm sm:grid-cols-2">
|
|
114
|
+
<div v-if="doc.doctype" class="flex gap-2">
|
|
115
|
+
<dt class="w-28 shrink-0 text-slate-500 dark:text-slate-400">Type</dt>
|
|
116
|
+
<dd>{{ doc.doctype }}</dd>
|
|
117
|
+
</div>
|
|
118
|
+
<div v-if="doc.stage" class="flex gap-2">
|
|
119
|
+
<dt class="w-28 shrink-0 text-slate-500 dark:text-slate-400">Stage</dt>
|
|
120
|
+
<dd>{{ doc.stage }}</dd>
|
|
121
|
+
</div>
|
|
122
|
+
<div v-if="doc.edition" class="flex gap-2">
|
|
123
|
+
<dt class="w-28 shrink-0 text-slate-500 dark:text-slate-400">Edition</dt>
|
|
124
|
+
<dd>{{ doc.edition }}</dd>
|
|
125
|
+
</div>
|
|
126
|
+
<div v-if="languages" class="flex gap-2">
|
|
127
|
+
<dt class="w-28 shrink-0 text-slate-500 dark:text-slate-400">Language</dt>
|
|
128
|
+
<dd>{{ languages }}</dd>
|
|
129
|
+
</div>
|
|
130
|
+
<div v-if="doc.publisher" class="flex gap-2">
|
|
131
|
+
<dt class="w-28 shrink-0 text-slate-500 dark:text-slate-400">Publisher</dt>
|
|
132
|
+
<dd>{{ doc.publisher }}</dd>
|
|
133
|
+
</div>
|
|
134
|
+
<div v-if="showSummaryDate" class="flex gap-2">
|
|
135
|
+
<dt class="w-28 shrink-0 text-slate-500 dark:text-slate-400">Date</dt>
|
|
136
|
+
<dd>{{ doc.date }}</dd>
|
|
137
|
+
</div>
|
|
138
|
+
</dl>
|
|
139
|
+
</section>
|
|
140
|
+
|
|
141
|
+
<section v-if="keywords.length" class="mb-6">
|
|
142
|
+
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
143
|
+
Keywords
|
|
144
|
+
</h3>
|
|
145
|
+
<div class="flex flex-wrap gap-1.5">
|
|
146
|
+
<span
|
|
147
|
+
v-for="k in keywords"
|
|
148
|
+
:key="k"
|
|
149
|
+
class="rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
|
150
|
+
>{{ k }}</span>
|
|
151
|
+
</div>
|
|
152
|
+
</section>
|
|
153
|
+
|
|
154
|
+
<section v-if="contributors.length" class="mb-6">
|
|
155
|
+
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
156
|
+
Contributors
|
|
157
|
+
</h3>
|
|
158
|
+
<ul class="space-y-1 text-sm">
|
|
159
|
+
<li v-for="(c, i) in contributors" :key="i" class="flex flex-wrap items-baseline gap-2">
|
|
160
|
+
<span class="text-slate-700 dark:text-slate-300">{{ c.name }}</span>
|
|
161
|
+
<span v-if="c.role" class="text-xs text-slate-500 dark:text-slate-400">{{ c.role }}</span>
|
|
162
|
+
</li>
|
|
163
|
+
</ul>
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
<section v-if="docids.length" class="mb-6">
|
|
167
|
+
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
168
|
+
Identifiers
|
|
169
|
+
</h3>
|
|
170
|
+
<ul class="space-y-1 text-sm">
|
|
171
|
+
<li v-for="(d, i) in docids" :key="i" class="flex flex-wrap items-baseline gap-2">
|
|
172
|
+
<span class="font-mono text-slate-700 dark:text-slate-300">{{ d.id }}</span>
|
|
173
|
+
<span v-if="docidLabel(d)" class="text-xs text-slate-500 dark:text-slate-400">{{ docidLabel(d) }}</span>
|
|
174
|
+
</li>
|
|
175
|
+
</ul>
|
|
176
|
+
</section>
|
|
177
|
+
|
|
178
|
+
<section v-if="relations.length" class="mb-6">
|
|
179
|
+
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
180
|
+
Relations
|
|
181
|
+
</h3>
|
|
182
|
+
<ul class="space-y-1 text-sm">
|
|
183
|
+
<li v-for="(r, i) in relations" :key="i" class="flex flex-wrap items-baseline gap-2">
|
|
184
|
+
<span class="w-28 shrink-0 text-slate-500 dark:text-slate-400">{{ r.label }}</span>
|
|
185
|
+
<!-- A real ?doc= anchor whose plain click App.vue turns into an
|
|
186
|
+
in-page navigation. `.exact` leaves modified clicks (Cmd/Ctrl/Shift
|
|
187
|
+
= open in new tab/window) to the browser, so the href stays useful. -->
|
|
188
|
+
<a
|
|
189
|
+
v-if="r.href"
|
|
190
|
+
:href="r.href"
|
|
191
|
+
class="font-mono text-brand hover:underline dark:text-brand-dark"
|
|
192
|
+
@click.exact.prevent="$emit('open', r.id)"
|
|
193
|
+
>{{ r.id }}</a>
|
|
194
|
+
<span v-else class="font-mono text-slate-700 dark:text-slate-300">{{ r.id }}</span>
|
|
195
|
+
</li>
|
|
196
|
+
</ul>
|
|
197
|
+
</section>
|
|
198
|
+
|
|
199
|
+
<section v-if="dates.length" class="mb-6">
|
|
200
|
+
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
201
|
+
Dates
|
|
202
|
+
</h3>
|
|
203
|
+
<ul class="space-y-1 text-sm">
|
|
204
|
+
<li v-for="(d, i) in dates" :key="i" class="flex flex-wrap items-baseline gap-2">
|
|
205
|
+
<span v-if="d.type" class="text-slate-500 dark:text-slate-400">{{ d.type }}</span>
|
|
206
|
+
<time class="text-slate-700 dark:text-slate-300">{{ d.value }}</time>
|
|
207
|
+
</li>
|
|
208
|
+
</ul>
|
|
209
|
+
</section>
|
|
210
|
+
|
|
211
|
+
<section v-if="doc.link || doc.yaml" class="flex flex-wrap gap-4 text-sm">
|
|
212
|
+
<a
|
|
213
|
+
v-if="doc.link"
|
|
214
|
+
:href="doc.link"
|
|
215
|
+
target="_blank"
|
|
216
|
+
rel="noopener"
|
|
217
|
+
class="inline-flex items-center gap-1 text-brand hover:underline dark:text-brand-dark"
|
|
218
|
+
><Icon name="external" class="h-4 w-4" />Landing page</a>
|
|
219
|
+
<a
|
|
220
|
+
v-if="doc.yaml"
|
|
221
|
+
:href="doc.yaml"
|
|
222
|
+
target="_blank"
|
|
223
|
+
rel="noopener"
|
|
224
|
+
class="inline-flex items-center gap-1 text-brand hover:underline dark:text-brand-dark"
|
|
225
|
+
><Icon name="document" class="h-4 w-4" />Raw YAML</a>
|
|
226
|
+
</section>
|
|
227
|
+
</article>
|
|
228
|
+
</template>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import type { IndexDocument } from "../lib/types";
|
|
4
|
+
import { docHref } from "../lib/url";
|
|
5
|
+
import Icon from "./Icon.vue";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ doc: IndexDocument; view: "list" | "grid" }>();
|
|
8
|
+
defineEmits<{ open: [id: string] }>();
|
|
9
|
+
|
|
10
|
+
// A real, shareable link to the detail page (?doc=<id>) — crawlable and
|
|
11
|
+
// middle-click/new-tab friendly — while a plain click is intercepted by App.vue
|
|
12
|
+
// to SPA-navigate (preserving the current ?page= etc.).
|
|
13
|
+
const detailHref = computed(() => docHref(props.doc.id));
|
|
14
|
+
|
|
15
|
+
const copied = ref(false);
|
|
16
|
+
async function copyId(id: string) {
|
|
17
|
+
try {
|
|
18
|
+
await navigator.clipboard.writeText(id);
|
|
19
|
+
copied.value = true;
|
|
20
|
+
setTimeout(() => (copied.value = false), 1200);
|
|
21
|
+
} catch {
|
|
22
|
+
/* clipboard may be blocked; ignore */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<article
|
|
29
|
+
class="document group py-4"
|
|
30
|
+
:class="view === 'grid'
|
|
31
|
+
? 'rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900'
|
|
32
|
+
: ''"
|
|
33
|
+
>
|
|
34
|
+
<div class="flex items-start justify-between gap-3">
|
|
35
|
+
<div class="min-w-0">
|
|
36
|
+
<div class="flex items-center gap-2">
|
|
37
|
+
<h2 class="truncate font-mono text-sm font-semibold text-brand dark:text-brand-dark">
|
|
38
|
+
<a
|
|
39
|
+
:href="detailHref"
|
|
40
|
+
class="hover:underline"
|
|
41
|
+
@click.exact.prevent="$emit('open', doc.id)"
|
|
42
|
+
>{{ doc.id }}</a>
|
|
43
|
+
</h2>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
class="rounded p-0.5 text-slate-400 opacity-0 transition group-hover:opacity-100 hover:text-brand"
|
|
47
|
+
:aria-label="`Copy DocID ${doc.id}`"
|
|
48
|
+
:title="copied ? 'Copied!' : 'Copy DocID'"
|
|
49
|
+
@click="copyId(doc.id)"
|
|
50
|
+
>
|
|
51
|
+
<Icon :name="copied ? 'check' : 'copy'" class="h-4 w-4" />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
<p class="mt-1 text-sm text-slate-700 dark:text-slate-300">
|
|
55
|
+
<a
|
|
56
|
+
:href="detailHref"
|
|
57
|
+
class="hover:underline"
|
|
58
|
+
tabindex="-1"
|
|
59
|
+
@click.exact.prevent="$emit('open', doc.id)"
|
|
60
|
+
>{{ doc.title }}</a>
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="flex shrink-0 items-start gap-2">
|
|
65
|
+
<div class="flex flex-col items-end gap-1 text-xs">
|
|
66
|
+
<div class="flex flex-wrap justify-end gap-1">
|
|
67
|
+
<span
|
|
68
|
+
v-if="doc.doctype"
|
|
69
|
+
class="rounded bg-slate-100 px-1.5 py-0.5 text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
|
70
|
+
>{{ doc.doctype }}</span>
|
|
71
|
+
<span
|
|
72
|
+
v-if="doc.stage"
|
|
73
|
+
class="rounded bg-emerald-100 px-1.5 py-0.5 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300"
|
|
74
|
+
>{{ doc.stage }}</span>
|
|
75
|
+
</div>
|
|
76
|
+
<time v-if="doc.date" class="text-slate-400">{{ doc.date }}</time>
|
|
77
|
+
</div>
|
|
78
|
+
<a
|
|
79
|
+
:href="detailHref"
|
|
80
|
+
class="mt-0.5 text-slate-300 transition group-hover:text-brand dark:text-slate-600 dark:group-hover:text-brand-dark"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
tabindex="-1"
|
|
83
|
+
@click.exact.prevent="$emit('open', doc.id)"
|
|
84
|
+
><Icon name="chevron-right" class="h-5 w-5" /></a>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</article>
|
|
88
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { mount } from "@vue/test-utils";
|
|
4
|
+
import Icon from "./Icon.vue";
|
|
5
|
+
|
|
6
|
+
describe("Icon", () => {
|
|
7
|
+
it("renders an inline SVG with a path for a known name", () => {
|
|
8
|
+
const w = mount(Icon, { props: { name: "search" } });
|
|
9
|
+
const svg = w.find("svg");
|
|
10
|
+
expect(svg.exists()).toBe(true);
|
|
11
|
+
expect(svg.find("path").exists()).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("uses the CalConnect Heroicons-outline style", () => {
|
|
15
|
+
const svg = mount(Icon, { props: { name: "search" } }).find("svg");
|
|
16
|
+
expect(svg.attributes("stroke")).toBe("currentColor");
|
|
17
|
+
expect(svg.attributes("fill")).toBe("none");
|
|
18
|
+
expect(svg.attributes("viewBox")).toBe("0 0 24 24");
|
|
19
|
+
// Decorative — the parent control carries the accessible label.
|
|
20
|
+
expect(svg.attributes("aria-hidden")).toBe("true");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders nothing for an unknown name", () => {
|
|
24
|
+
const w = mount(Icon, { props: { name: "not-a-real-icon" } });
|
|
25
|
+
expect(w.find("svg").exists()).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("passes a caller-supplied class through to the svg root", () => {
|
|
29
|
+
const w = mount(Icon, { props: { name: "sun" }, attrs: { class: "h-5 w-5" } });
|
|
30
|
+
const svg = w.find("svg");
|
|
31
|
+
expect(svg.classes()).toContain("h-5");
|
|
32
|
+
expect(svg.classes()).toContain("w-5");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("provides every icon the UI references", () => {
|
|
36
|
+
const names = [
|
|
37
|
+
"sun", "moon", "search", "chevron-up", "chevron-down",
|
|
38
|
+
"list", "grid", "copy", "check", "document",
|
|
39
|
+
];
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
expect(mount(Icon, { props: { name } }).find("path").exists()).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
|
|
4
|
+
// Inline Heroicons-outline SVG paths, matching the icon set used on
|
|
5
|
+
// https://standards.calconnect.org/standards/ (fill=none, stroke=currentColor,
|
|
6
|
+
// 24×24). Keeping the geometry here — instead of an icon dependency — mirrors
|
|
7
|
+
// how CalConnect inlines the same paths, so the icons inherit text colour and
|
|
8
|
+
// dark mode for free. Callers size the icon with a passthrough class (h-5 w-5…)
|
|
9
|
+
// and provide the accessible label on the surrounding control; the glyph itself
|
|
10
|
+
// stays aria-hidden.
|
|
11
|
+
const PATHS: Record<string, string[]> = {
|
|
12
|
+
sun: [
|
|
13
|
+
"M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
|
|
14
|
+
],
|
|
15
|
+
moon: [
|
|
16
|
+
"M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z",
|
|
17
|
+
],
|
|
18
|
+
search: ["M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"],
|
|
19
|
+
"chevron-up": ["M5 15l7-7 7 7"],
|
|
20
|
+
"chevron-down": ["M19 9l-7 7-7-7"],
|
|
21
|
+
"chevron-right": ["M9 5l7 7-7 7"],
|
|
22
|
+
"arrow-left": ["M10 19l-7-7m0 0l7-7m-7 7h18"],
|
|
23
|
+
external: ["M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"],
|
|
24
|
+
list: ["M4 6h16M4 12h16M4 18h16"],
|
|
25
|
+
grid: [
|
|
26
|
+
"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z",
|
|
27
|
+
],
|
|
28
|
+
copy: [
|
|
29
|
+
"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z",
|
|
30
|
+
],
|
|
31
|
+
check: ["M5 13l4 4L19 7"],
|
|
32
|
+
document: [
|
|
33
|
+
"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const props = defineProps<{ name: string }>();
|
|
38
|
+
const paths = computed(() => PATHS[props.name] ?? null);
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<svg
|
|
43
|
+
v-if="paths"
|
|
44
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
stroke-width="2"
|
|
48
|
+
stroke-linecap="round"
|
|
49
|
+
stroke-linejoin="round"
|
|
50
|
+
viewBox="0 0 24 24"
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
>
|
|
53
|
+
<path v-for="(d, i) in paths" :key="i" :d="d" />
|
|
54
|
+
</svg>
|
|
55
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{ page: number; pageCount: number }>();
|
|
3
|
+
const emit = defineEmits<{ go: [page: number] }>();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<nav
|
|
8
|
+
v-if="pageCount > 1"
|
|
9
|
+
class="flex items-center justify-center gap-2 text-sm"
|
|
10
|
+
aria-label="Pagination"
|
|
11
|
+
>
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
class="rounded-md border border-slate-300 px-3 py-1 disabled:opacity-40 dark:border-slate-700"
|
|
15
|
+
:disabled="page === 1"
|
|
16
|
+
@click="emit('go', page - 1)"
|
|
17
|
+
>Prev</button>
|
|
18
|
+
<span class="text-slate-500 dark:text-slate-400">Page {{ page }} of {{ pageCount }}</span>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
class="rounded-md border border-slate-300 px-3 py-1 disabled:opacity-40 dark:border-slate-700"
|
|
22
|
+
:disabled="page === pageCount"
|
|
23
|
+
@click="emit('go', page + 1)"
|
|
24
|
+
>Next</button>
|
|
25
|
+
</nav>
|
|
26
|
+
</template>
|