diff --git a/.gitignore b/.gitignore index db48b9f2..6f3e4ffe 100644 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,18 @@ apps/static/css/project.css apps/static/js/main.js apps/static/js/main.js.map +# django-sass-processor compiles these from the adjacent .scss at request time +# (or via `manage.py compilescss` for production). A committed .css here would +# shadow that compilation, since FileSystemFinder is checked before CssFinder. +apps/static/css/readux.css +apps/static/css/components/collection.css +apps/static/css/components/flatpage.css +apps/static/css/components/login.css +apps/static/css/components/menu-inverse.css +apps/static/css/components/reader.css +apps/static/css/components/search.css +apps/static/css/components/user-form.css + snippets/* diff --git a/apps/iiif/manifests/documents.py b/apps/iiif/manifests/documents.py index b261a8b4..4d224df7 100644 --- a/apps/iiif/manifests/documents.py +++ b/apps/iiif/manifests/documents.py @@ -9,6 +9,9 @@ from django_elasticsearch_dsl import Document, fields from django_elasticsearch_dsl.registries import registry +from edtf import parse_edtf +from edtf.convert import struct_time_to_jd +from edtf.natlang import text_to_edtf from elasticsearch_dsl import MetaField, Keyword, analyzer from unidecode import unidecode @@ -24,6 +27,32 @@ ) +def _published_date_fallback_jd(instance, bound): + """date_earliest/date_latest are derived from published_date_edtf, a + separate field cataloguers are expected to fill in alongside the + display-only published_date. In practice a lot of records only ever get + published_date set, which silently excludes them from date-published + filtering/sorting and miscounts them as "no date" even though they show + a real date on screen. Fall back to parsing published_date itself so the + search filter reflects what's actually displayed. + """ + if not instance.published_date: + return None + try: + edtf_string = text_to_edtf(instance.published_date) + if not edtf_string: + return None + edtf_obj = parse_edtf(edtf_string, fail_silently=True) + if not edtf_obj: + return None + struct_time = edtf_obj.lower_fuzzy() if bound == "lower" else edtf_obj.upper_fuzzy() + return struct_time_to_jd(struct_time) + except Exception: # pylint: disable=broad-except + # published_date is free-text catalog data; malformed/unparseable + # values should degrade to "no fallback date", not break indexing. + return None + + @registry.register_document class ManifestDocument(Document): """Elasticsearch Document class for IIIF Manifest""" @@ -124,6 +153,20 @@ def prepare_has_pdf(self, instance): """convert pdf field into boolean""" return bool(instance.pdf) + def prepare_date_earliest(self, instance): + """Fall back to parsing the display-only published_date when + date_earliest is unset (see _published_date_fallback_jd)""" + if instance.date_earliest is not None: + return instance.date_earliest + return _published_date_fallback_jd(instance, "lower") + + def prepare_date_latest(self, instance): + """Fall back to parsing the display-only published_date when + date_latest is unset (see _published_date_fallback_jd)""" + if instance.date_latest is not None: + return instance.date_latest + return _published_date_fallback_jd(instance, "upper") + def prepare_label_alphabetical(self, instance): """get the first 64 chars of a label, just for sorting purposes""" if instance.label: diff --git a/apps/iiif/manifests/tests/test_documents.py b/apps/iiif/manifests/tests/test_documents.py index e1965564..8aaa143a 100644 --- a/apps/iiif/manifests/tests/test_documents.py +++ b/apps/iiif/manifests/tests/test_documents.py @@ -94,6 +94,42 @@ def test_prepare_summary(self): manifest.summary = "

Has HTML tags

" assert self.doc.prepare_summary(instance=manifest) == "Has HTML tags" + def test_prepare_date_earliest_and_latest_use_model_value_when_set(self): + """Should use the model's own date_earliest/date_latest when present, + not the published_date fallback""" + manifest = ManifestFactory.create(published_date_edtf="2022-04-14") + manifest.refresh_from_db() + assert manifest.date_earliest is not None + assert self.doc.prepare_date_earliest(instance=manifest) == manifest.date_earliest + assert self.doc.prepare_date_latest(instance=manifest) == manifest.date_latest + + def test_prepare_date_earliest_and_latest_fall_back_to_published_date(self): + """A manifest with only the display-only published_date set (no + published_date_edtf) should still get real date_earliest/date_latest + values for search, instead of being silently excluded/miscounted as + undated""" + manifest = ManifestFactory.create(published_date="1997-07-20") + manifest.refresh_from_db() + assert manifest.date_earliest is None + assert manifest.date_latest is None + + earliest = self.doc.prepare_date_earliest(instance=manifest) + latest = self.doc.prepare_date_latest(instance=manifest) + assert earliest is not None + assert latest is not None + assert earliest == latest # exact single date, no fuzziness + + def test_prepare_date_earliest_and_latest_none_for_unparseable_or_missing(self): + """Should return None (not raise) when published_date is missing or + can't be parsed as a date at all""" + manifest = ManifestFactory.create(published_date=None) + assert self.doc.prepare_date_earliest(instance=manifest) is None + assert self.doc.prepare_date_latest(instance=manifest) is None + + manifest.published_date = "S.l. : s.n." + assert self.doc.prepare_date_earliest(instance=manifest) is None + assert self.doc.prepare_date_latest(instance=manifest) is None + def test_get_queryset(self): """Test prefetching""" manifest = ManifestFactory.create() diff --git a/apps/readux/forms.py b/apps/readux/forms.py index f7c71540..ed10a33e 100644 --- a/apps/readux/forms.py +++ b/apps/readux/forms.py @@ -169,6 +169,11 @@ class ManifestSearchForm(forms.Form): format="%Y-%m-%d", ), ) + include_undated = forms.BooleanField( + label="Show volumes without a published date", + required=False, + widget=forms.CheckboxInput(attrs={"class": "uk-checkbox"}), + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/apps/readux/tests/test_views.py b/apps/readux/tests/test_views.py index 557524b9..e37e2c28 100644 --- a/apps/readux/tests/test_views.py +++ b/apps/readux/tests/test_views.py @@ -220,6 +220,67 @@ def test_get_queryset_filters(self): response = search_results.execute(ignore_cache=True) assert response.hits.total["value"] == 1 + def test_get_queryset_date_range_excludes_undated_by_default(self): + """A date filter should exclude volumes with no published date unless + include_undated is set""" + undated = Manifest( + pid="uniquepid-undated", + label="undated volume", + summary="test", + author="Ben", + ) + undated.save() + ManifestDocument().update(undated, True, "index") + + volume_search_view = views.VolumeSearchView() + volume_search_view.request = Mock() + + # date filter active, include_undated not set: undated volume excluded + volume_search_view.request.GET = { + "start_date": "2020-01-01", + "end_date": "2024-01-01", + } + search_results = volume_search_view.get_queryset() + response = search_results.execute(ignore_cache=True) + pids = {hit["pid"] for hit in response.hits} + assert response.hits.total["value"] == 2 + assert undated.pid not in pids + + # date filter active, include_undated set: undated volume included + # alongside anything actually within the date range + volume_search_view.request.GET = { + "start_date": "2020-01-01", + "end_date": "2024-01-01", + "include_undated": "on", + } + search_results = volume_search_view.get_queryset() + response = search_results.execute(ignore_cache=True) + pids = {hit["pid"] for hit in response.hits} + assert response.hits.total["value"] == 3 + assert undated.pid in pids + + def test_get_queryset_date_aggregation_unaffected_by_date_filter(self): + """The min/max date aggregation used to populate the year dropdowns + should reflect the full available range, not shrink to whatever date + filter is currently applied — otherwise a "reset to full range" + control could never recover the original bounds.""" + volume_search_view = views.VolumeSearchView() + volume_search_view.request = Mock() + + volume_search_view.request.GET = { + "start_date": "2022-01-01", + "end_date": "2022-12-31", + } + search_results = volume_search_view.get_queryset() + response = search_results.execute(ignore_cache=True) + # aggregation should span all three dated volumes (1900-2022), not + # just the ones matching the applied 2022 date filter + in_scope = response.aggregations.date_range_scope.in_scope + assert in_scope.dated_earliest.min_date.value is not None + assert in_scope.dated_latest.max_date.value is not None + span_days = in_scope.dated_latest.max_date.value - in_scope.dated_earliest.min_date.value + assert span_days > 365 * 100 # spans well over a century (1900-2022) + def test_get_queryset_sorting(self): """Should sort according to default or chosen sort""" volume_search_view = views.VolumeSearchView() @@ -317,10 +378,11 @@ def test_get_context_data(self, mock_set_date, mock_set_facets): # these are not nested facets, so delete "inner" attributes del response.aggregations.language.inner del response.aggregations.author.inner + response.aggregations.date_range_scope.in_scope.undated.doc_count = 5 from datetime import date with patch("apps.readux.views.jd_to_date", side_effect=[date(1800, 1, 1), date(2022, 12, 31)]): - volume_search_view.get_context_data() + context_data = volume_search_view.get_context_data() mock_set_facets.assert_called_with( { "language": response.aggregations.language.buckets, @@ -330,3 +392,36 @@ def test_get_context_data(self, mock_set_date, mock_set_facets): } ) mock_set_date.assert_called_with("1800-01-01", "2022-12-31") + # min date resolved to a real (non-None) date, so no BCE clamping happened + assert context_data["date_range_has_bce"] is False + assert context_data["undated_volume_count"] == 5 + + @patch("apps.readux.forms.ManifestSearchForm.set_facets") + @patch("apps.readux.forms.ManifestSearchForm.set_date") + def test_get_context_data_flags_bce_clamping(self, mock_set_date, mock_set_facets): + """Should flag when the earliest date had to be clamped to year 1 + because the true minimum is BCE (unrepresentable as a Python date)""" + volume_search_view = views.VolumeSearchView(kwargs={}) + volume_search_view.request = Mock() + volume_search_view.request.GET = {} + volume_search_view.facets = [ + ("language", Mock()), + ("author", Mock()), + ("collections", Mock()), + ] + with patch("apps.readux.views.VolumeSearchView.get_queryset") as mock_queryset: + volume_search_view.queryset = mock_queryset + volume_search_view.object_list = mock_queryset + response = Mock() + mock_queryset.return_value.__getitem__.return_value.execute.return_value = response + del response.aggregations.language.inner + del response.aggregations.author.inner + response.aggregations.date_range_scope.in_scope.undated.doc_count = 0 + + from datetime import date + # jd_to_date returns None for the min (BCE, unrepresentable), a real + # date for the max + with patch("apps.readux.views.jd_to_date", side_effect=[None, date(2022, 12, 31)]): + context_data = volume_search_view.get_context_data() + mock_set_date.assert_called_with("0001-01-01", "2022-12-31") + assert context_data["date_range_has_bce"] is True diff --git a/apps/readux/views.py b/apps/readux/views.py index adace0b7..47653aa7 100644 --- a/apps/readux/views.py +++ b/apps/readux/views.py @@ -452,11 +452,15 @@ def get_context_data(self, **kwargs): context_data["form"].set_facets(facets) # get min and max date aggregations and set on form. - # these are nested under filter buckets (dated_earliest / dated_latest) so that - # undated volumes are excluded from the aggregation range. - aggs = volumes_response.aggregations - dated_earliest = getattr(aggs, "dated_earliest", None) - dated_latest = getattr(aggs, "dated_latest", None) + # nested under date_range_scope.in_scope (see get_queryset) so the range + # reflects every active filter except the date range itself, and further + # under dated_earliest/dated_latest filter buckets so undated volumes are + # excluded from the aggregation range. + date_range_scope = getattr(volumes_response.aggregations, "date_range_scope", None) + in_scope = getattr(date_range_scope, "in_scope", None) if date_range_scope else None + dated_earliest = getattr(in_scope, "dated_earliest", None) if in_scope else None + dated_latest = getattr(in_scope, "dated_latest", None) if in_scope else None + context_data["date_range_has_bce"] = False if dated_earliest and dated_latest: min_date = getattr(dated_earliest, "min_date", None) max_date = getattr(dated_latest, "max_date", None) @@ -468,8 +472,12 @@ def get_context_data(self, **kwargs): # jd_to_date returns None for pre-CE dates (year 0 / 1 BC) because # Python's date.MINYEAR = 1. Clamp to date(1, 1, 1) so the slider # has a valid lower bound; BCE volumes remain indexed and searchable. + # Flag this on context so the template/JS can tell the user their + # "1" option actually stands in for "1 or earlier (BCE included)". from datetime import date as _date - min_d = jd_to_date(min_jd) or _date(1, 1, 1) + clamped_min_d = jd_to_date(min_jd) + context_data["date_range_has_bce"] = clamped_min_d is None + min_d = clamped_min_d or _date(1, 1, 1) max_d = jd_to_date(max_jd) if max_d: # strftime("%Y") does not zero-pad years < 1000 on all platforms, @@ -478,6 +486,11 @@ def _fmt(d): return f"{d.year:04d}-{d.month:02d}-{d.day:02d}" context_data["form"].set_date(_fmt(min_d), _fmt(max_d)) + # count of volumes with no published date at all (matching every other + # active filter), for the "show volumes without a date" checkbox label + undated = getattr(in_scope, "undated", None) if in_scope else None + context_data["undated_volume_count"] = getattr(undated, "doc_count", 0) if undated else 0 + # Attach start_canvas to each volume in the current page. # Handle both: paginator page and raw list-like. vol_page = context_data.get("volumes") @@ -639,6 +652,18 @@ def get_queryset(self): query=Q("terms", **{"collections.label": collection_filter}), ) + # Capture the query/filter state *before* the date-range filter below is + # applied — Elasticsearch scopes all non-global aggregations by the final + # request's full query+filter body, regardless of the Python call order + # used to build it, so simply adding the aggs earlier doesn't exempt them + # from a date filter added later. Reusing this captured query inside a + # "global" aggregation (which ignores query context entirely on its own) + # is what actually lets the min/max date aggregation reflect the full + # available range — respecting every other active filter (search terms, + # author, language, collection) but not the date range filter itself — + # so a "reset to full range" control has something real to reset to. + pre_date_query = volumes.to_dict().get("query", {"match_all": {}}) + # filter on date published # Date range overlap logic: include a document if its date window overlaps the # filter window. A document [earliest, latest] overlaps [start, end] when: @@ -647,15 +672,34 @@ def get_queryset(self): # if date_earliest is null we can't exclude the document from an end_date filter, # and if date_latest is null we can't confirm it falls after a start_date. min_date_filter = form_data.get("start_date") - if min_date_filter: - min_jd = date_to_jd(date(min_date_filter.year, 1, 1)) - volumes = volumes.filter("exists", field="date_latest") - volumes = volumes.filter("range", date_latest={"gte": min_jd}) max_date_filter = form_data.get("end_date") or "" - if max_date_filter: - max_jd = date_to_jd(date(max_date_filter.year, 12, 31)) - volumes = volumes.filter("exists", field="date_earliest") - volumes = volumes.filter("range", date_earliest={"lte": max_jd}) + if min_date_filter or max_date_filter: + date_range_clauses = [] + if min_date_filter: + min_jd = date_to_jd(date(min_date_filter.year, 1, 1)) + date_range_clauses.append(Q("exists", field="date_latest")) + date_range_clauses.append(Q("range", date_latest={"gte": min_jd})) + if max_date_filter: + max_jd = date_to_jd(date(max_date_filter.year, 12, 31)) + date_range_clauses.append(Q("exists", field="date_earliest")) + date_range_clauses.append(Q("range", date_earliest={"lte": max_jd})) + date_range_query = Q("bool", must=date_range_clauses) + + if form_data.get("include_undated"): + # let volumes with no date at all through too, alongside anything + # that actually overlaps the selected range + undated_query = Q( + "bool", + must_not=[ + Q("exists", field="date_earliest"), + Q("exists", field="date_latest"), + ], + ) + volumes = volumes.filter( + Q("bool", should=[date_range_query, undated_query], minimum_should_match=1) + ) + else: + volumes = volumes.filter(date_range_query) # filter on custom metadata fields if hasattr(settings, "CUSTOM_METADATA") and isinstance( @@ -680,15 +724,34 @@ def get_queryset(self): volumes.aggs.bucket(facet_name, facet.get_aggregation()) # get min and max date published values, excluding volumes with no dates. - # Some undated volumes store 0 rather than null, so an exists filter is not - # enough — use a range filter requiring a positive JD value (JD > 0 means - # after 4713 BC; any plausible publication date will satisfy this). - volumes.aggs.bucket( + # Wrapped in a "global" bucket so it ignores the query context entirely, + # then re-scoped via `pre_date_query` (captured above) to respect every + # other active filter except the date range itself. See the comment + # above `pre_date_query` for why this can't just be plain aggs. + volumes.aggs.bucket("date_range_scope", "global").bucket( + "in_scope", "filter", filter=pre_date_query + ) + in_scope_aggs = volumes.aggs["date_range_scope"]["in_scope"] + in_scope_aggs.bucket( "dated_earliest", "filter", filter={"exists": {"field": "date_earliest"}} ).metric("min_date", "min", field="date_earliest") - volumes.aggs.bucket( + in_scope_aggs.bucket( "dated_latest", "filter", filter={"exists": {"field": "date_latest"}} ).metric("max_date", "max", field="date_latest") + # count of volumes with no published date at all, matching every other + # active filter — powers the "N volumes without a published date" label + in_scope_aggs.bucket( + "undated", + "filter", + filter={ + "bool": { + "must_not": [ + {"exists": {"field": "date_earliest"}}, + {"exists": {"field": "date_latest"}}, + ] + } + }, + ) # sort volumes = volumes.sort(form_data["sort"]) diff --git a/apps/static/css/components/collection.css b/apps/static/css/components/collection.css deleted file mode 100644 index 3d378c8f..00000000 --- a/apps/static/css/components/collection.css +++ /dev/null @@ -1,305 +0,0 @@ -.full-width-bg { - background-size: cover; - background-position: center; - position: relative; - height: auto; - color: #ffffff; } - -/* Hero Container - Two Column Layout */ -.hero-container { - display: flex; - align-items: stretch; - min-height: 400px; } - @media (max-width: 768px) { - .hero-container { - flex-direction: column; - min-height: auto; } } -/* Left Column - Text Content */ -.hero-text-column { - flex: 1; - background: linear-gradient(to right, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.7) 100%); - padding: 50px; - box-sizing: border-box; - display: flex; - flex-direction: column; - justify-content: flex-start; - color: #ffffff; - position: relative; - z-index: 2; } - @media (max-width: 768px) { - .hero-text-column { - padding: 30px; - min-height: auto; } } -/* Collection Title - Responsive Font Size */ -.collection-title { - font-size: 2.5rem; - margin-bottom: 15px; - color: #ffffff; - word-wrap: break-word; - overflow-wrap: break-word; - line-height: 1.2; - /* When title is very long, reduce font size */ - /* When title is truncated */ } - @media (max-width: 1200px) { - .collection-title { - font-size: 2rem; } } - @media (max-width: 768px) { - .collection-title { - font-size: 1.75rem; } } - .collection-title.title-long { - font-size: 1.75rem; } - @media (max-width: 1200px) { - .collection-title.title-long { - font-size: 1.5rem; } } - .collection-title.title-truncated { - font-size: 1.5rem; } - @media (max-width: 1200px) { - .collection-title.title-truncated { - font-size: 1.25rem; } } -.rx-title-tagline { - font-size: 1rem; - color: rgba(255, 255, 255, 0.9); - font-weight: 500; } - -/* Collection Description */ -.collection-description { - font-size: 1rem; - line-height: 1.6; - color: rgba(255, 255, 255, 0.95); - margin-top: 20px; - margin-bottom: 25px; - flex-grow: 1; - /* Links need a light color for contrast against the dark hero background */ - /* When description is truncated */ } - .collection-description .description-text { - display: block; } - .collection-description a { - color: #77caff; - text-decoration: underline; } - .collection-description a:hover, .collection-description a:focus { - color: #F1FAEE; } - .collection-description.description-truncated .description-text::after { - content: ''; } - -/* Description Button */ -.description-button { - margin-top: auto; - background-color: #E60000; - border: none; - color: #ffffff; - font-weight: bold; - height: 40px; - padding: 0 20px; - border-radius: 5px; - font-size: 1em; - display: inline-flex; - align-items: center; - justify-content: center; - transition: background-color 0.3s ease; - cursor: pointer; - width: fit-content; } - .description-button:hover { - background-color: #b30000; } - -/* Right Column - Image and Info Icon */ -.hero-image-column { - flex: 1; - background-size: cover; - background-position: center; - position: relative; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; } - @media (max-width: 768px) { - .hero-image-column { - min-height: 300px; } } -.hero-image-wrapper { - position: relative; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; } - -.image-placeholder { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.2); - z-index: 1; } - -/* Info Icon Button */ -.info-icon-button { - position: absolute; - bottom: 20px; - left: 20px; - z-index: 3; - background: transparent; - border: none; - border-radius: 50%; - width: 60px; - height: 60px; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.3s ease; - color: rgba(255, 255, 255, 0.8); } - .info-icon-button span { - display: flex; - align-items: center; - justify-content: center; } - .info-icon-button:hover { - background: rgba(255, 255, 255, 0.25); - border: 2px solid rgba(255, 255, 255, 0.7); - color: #ffffff; - box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); } - @media (max-width: 768px) { - .info-icon-button { - bottom: 15px; - left: 15px; - width: 50px; - height: 50px; } - .info-icon-button span { - font-size: 1.2rem; } } -/* Modal Styles */ -.rx-collection-modal h1, -.rx-collection-modal h2, -.rx-collection-modal h3, -.rx-collection-modal h4, -.rx-collection-modal h5, -.rx-collection-modal h6, -.rx-collection-modal p, -.rx-collection-modal div { - color: #ffffff; } - -.rx-collection-modal h2 { - margin-bottom: 0.5rem; - font-size: 2rem; } - -.rx-collection-modal h3 { - margin-top: 0; - margin-bottom: 1rem; - font-size: 1.1rem; } - -.rx-collection-modal a { - color: #77caff; - text-decoration: underline; - transition: color 0.3s ease; } - .rx-collection-modal a:hover, .rx-collection-modal a:active { - color: #aadeff; - text-decoration: underline; } - -.rx-collection-modal .uk-close { - color: #ffffff; - background: none; - border: none; } - .rx-collection-modal .uk-close:hover { - color: #e6e6e6; } - -.modal-bg { - background-size: cover; - background-position: center; - position: relative; - height: 100%; - width: 100%; - color: #ffffff; } - -.modal-overlay { - background: rgba(0, 0, 0, 0.85); - height: 100%; - width: 100%; - position: absolute; - top: 0; - left: 0; - padding: 30px; - box-sizing: border-box; - display: flex; - align-items: flex-start; - justify-content: center; - overflow-y: auto; } - @media (max-width: 768px) { - .modal-overlay { - padding: 20px; } } -.modal-content { - max-width: 1000px; - width: 100%; - margin-top: 50px; } - @media (max-width: 768px) { - .modal-content { - margin-top: 30px; } } -.modal-grid { - display: grid; - grid-template-columns: 2fr 1fr; - gap: 3rem; } - @media (max-width: 1024px) { - .modal-grid { - grid-template-columns: 1fr; - gap: 2rem; } } - @media (max-width: 768px) { - .modal-grid { - grid-template-columns: 1fr; - gap: 1.5rem; } } -.modal-description-section { - grid-column: 1; } - -.modal-metadata-section { - grid-column: 2; - padding-top: 6rem; } - @media (max-width: 1024px) { - .modal-metadata-section { - grid-column: 1; - padding-top: 0; } } -.modal-section { - margin-bottom: 0; } - -.volume-count { - font-size: 1rem; - color: rgba(255, 255, 255, 0.8); - margin-bottom: 1.5rem; - font-weight: 500; } - -.modal-description { - font-size: 1rem; - line-height: 1.8; - color: rgba(255, 255, 255, 0.95); - margin-bottom: 1rem; } - .modal-description p { - margin-bottom: 1rem; } - .modal-description p:last-child { - margin-bottom: 0; } - -.metadata-section { - padding-top: 0; - border-top: none; } - -.metadata-content { - display: grid; - grid-template-columns: 1fr; - gap: 0.75rem; } - -.metadata-item { - display: flex; - gap: 0.5rem; - font-size: 0.95rem; } - .metadata-item strong { - color: rgba(255, 255, 255, 0.9); - min-width: 200px; - flex-shrink: 0; } - @media (max-width: 768px) { - .metadata-item strong { - min-width: 150px; } } - .metadata-item span { - color: rgba(255, 255, 255, 0.85); - word-wrap: break-word; - overflow-wrap: break-word; } - -#modal-full p { - color: #ffffff; } - -#modal-full h2 { - font-size: xx-large; } diff --git a/apps/static/css/components/collection.scss b/apps/static/css/components/collection.scss index 4ff930e1..60f60271 100644 --- a/apps/static/css/components/collection.scss +++ b/apps/static/css/components/collection.scss @@ -9,6 +9,31 @@ color: $color-white; } +/* All-collections page, Banner layout */ +.collection-banner-list { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.collection-banner-item { + display: block; + text-decoration: none; + color: $color-white; + opacity: 0.9; + transition: opacity 0.15s ease; + border-radius: 4px; + overflow: hidden; + + &:hover, &:focus { + opacity: 1; + text-decoration: none; + color: $color-white; + } + + .hero-container { min-height: 250px; } +} + /* Hero Container - Two Column Layout */ .hero-container { display: flex; diff --git a/apps/static/css/components/flatpage.css b/apps/static/css/components/flatpage.css deleted file mode 100644 index 055ac04d..00000000 --- a/apps/static/css/components/flatpage.css +++ /dev/null @@ -1,128 +0,0 @@ -.navigation { - background-color: #F1FAEE; - padding: 30px; - position: sticky; - top: 20px; - border-radius: 10px 10px; - /* Rounded corners on the right-hand side */ - width: auto; - /* Fit to content width */ } - -.navigation a, -.navigation .uk-nav-default > li > a { - color: #1D3557; - text-decoration: none; - display: block; - padding: 5px 15px !important; - position: relative; - font-weight: normal; - font-size: medium; } - -.navigation a.active, -.navigation .uk-nav-default > li > a.active { - font-weight: bold; - color: #1D3557; } - .navigation a.active:hover, - .navigation .uk-nav-default > li > a.active:hover { - color: #101e31 !important; } - -.navigation a.active::before { - content: ''; - position: absolute; - left: 3px; - top: 50%; - transform: translateY(-50%); - height: 75%; - width: 4px; - background-color: #1D3557; } - -.content { - padding: 40px; } - -.section { - padding-bottom: 40px; } - -.static-content-title { - font-weight: bold; - margin: unset; } - -.content p, .content ul { - margin-top: 0; - margin-bottom: 1rem; - color: #000000; } - -.content ol, .content li { - margin-top: 0; - color: #000000; } - -.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { - margin-top: 1.25rem !important; - margin-bottom: 0.5rem !important; } - -.content h1 { - font-size: 2.5rem; } - -.content h2 { - font-size: 2rem; } - -.content h3 { - font-size: 1.5rem; } - -.content h4 { - font-size: 1.25rem; } - -.content h5 { - font-size: 1rem; } - -.content h6 { - font-size: 0.75rem; } - -.content li:first-of-type, -.content p:first-of-type { - margin-top: 0; } - -.content ul:first-of-type { - margin-top: 0.5rem; } - -.content li:last-of-type { - margin-bottom: 0; } - -/* a series of indentation for the side nav headers */ -.indent-h1 a { - color: #1D3557 !important; } - -.indent-h2 a { - color: #1D3557 !important; } - -.indent-h3 a { - color: #1D3557 !important; - margin-left: 1.5rem; } - -.indent-h4 a { - color: #1D3557 !important; - margin-left: 2.5rem; } - -.content ul, .content ol { - padding-left: 1.75rem !important; } - .content ul li, .content ol li { - margin-bottom: 0.5rem !important; } - -.content ul { - list-style: disc !important; } - -.content ol { - list-style: decimal !important; } - -.content a { - color: #E60000; - text-decoration: underline; } - -blockquote { - border-left: 4px solid #457B9D !important; - padding-left: 1rem !important; - color: #333333 !important; - margin: 1rem 0 !important; } - -/* prevent the sidebar from overlapping content */ -.sidebar-sticky { - z-index: 0; } diff --git a/apps/static/css/components/login.css b/apps/static/css/components/login.css deleted file mode 100644 index aebfcff2..00000000 --- a/apps/static/css/components/login.css +++ /dev/null @@ -1,88 +0,0 @@ -.custom-modal .uk-modal-dialog { - width: 400px; - padding: 20px; } - -.modal-header { - color: #1D3557; - padding: 20px; - text-align: center; } - -.modal-content { - padding: 20px; } - -.uk-input-icon { - display: flex; - align-items: center; } - -.uk-input-icon input { - padding-left: 40px; } - -.uk-input-icon span { - position: absolute; - padding-left: 10px; - color: #1D3557; } - -.uk-divider { - margin: 20px 0; } - -.sso-text { - margin-bottom: 16px; - text-align: center; } - -.help-link { - color: #1D3557; - display: block; - text-align: center; - margin-top: 20px; } - -.sso-buttons { - display: flex; - flex-direction: column; - gap: 10px; } - .sso-buttons form { - width: 100%; } - -.sso-button { - width: 100%; - display: inline-flex; - align-items: center; - gap: 12px; - padding: 0 1.25rem; - height: 44px; - border-radius: 4px; - font-size: 0.95rem; - font-weight: 500; - color: #ffffff; - background-color: #1D3557; - border: none; - transition: filter 0.15s ease; } - .sso-button:hover { - filter: brightness(1.15); - color: #ffffff; } - .sso-button .sso-button__icon { - flex-shrink: 0; - display: flex; - align-items: center; } - .sso-button span:last-child { - flex: 1; - text-align: center; } - .sso-button.sso-button--github { - background-color: #24292e; } - .sso-button.sso-button--google { - background-color: #4285F4; } - .sso-button.sso-button--twitter { - background-color: #1DA1F2; } - .sso-button.sso-button--facebook { - background-color: #1877F2; } - -.action-buttons { - margin-top: 20px; - display: flex; - justify-content: center; - gap: 10px; } - -.sign-in-btn { - background-color: #1D3557; - color: #F1FAEE; } - .sign-in-btn:hover { - background-color: #101e31; } diff --git a/apps/static/css/components/menu-inverse.css b/apps/static/css/components/menu-inverse.css deleted file mode 100644 index 7d5ba708..00000000 --- a/apps/static/css/components/menu-inverse.css +++ /dev/null @@ -1,37 +0,0 @@ -/* Inverse the color for content pages */ -.menu-item { - color: #1d3557 !important; - text-decoration: none !important; - transition: color 0.1s ease !important; } - .menu-item:hover { - color: rgba(29, 53, 87, 0.8) !important; } - .menu-item:active { - color: rgba(29, 53, 87, 0.6) !important; } - -.brand-logo { - font-size: large; - color: #1D3557 !important; - font-weight: bold; } - .brand-logo:hover { - color: rgba(29, 53, 87, 0.8) !important; } - .brand-logo:active { - color: rgba(29, 53, 87, 0.6) !important; } - -.brand-tagline { - font-size: small; - color: #1D3557; } - -.brand-readux { - color: #1D3557; } - .brand-readux:hover { - color: rgba(29, 53, 87, 0.8) !important; } - .brand-readux:active { - color: rgba(29, 53, 87, 0.6) !important; } - -.brand-inline { - font-size: small; - font-weight: normal; - text-decoration: underline !important; } - -.uk-navbar-container { - position: relative; } diff --git a/apps/static/css/components/reader.css b/apps/static/css/components/reader.css deleted file mode 100644 index 780219ed..00000000 --- a/apps/static/css/components/reader.css +++ /dev/null @@ -1,97 +0,0 @@ -.reader-navbar { - padding: 0 1rem; - height: 36px; } - -.rx-accordion-head { - background-color: #F1FAEE; - color: #1D3557; - border: none; } - .rx-accordion-head:hover { - color: #1D3557 !important; - background-color: #e1f4da !important; - border: none; } - .rx-accordion-head:hover .rx-accordion-head::before { - color: #1D3557 !important; } - .rx-accordion-head:active { - border: none; } - -.rx-accordion-head::before { - color: #1D3557 !important; } - -.rx-anchor { - color: #1D3557 !important; } - -.uk-tab > .uk-active > a { - border-color: #1D3557 !important; - color: #1D3557 !important; } - -.uk-search-default .uk-search-input:focus, .uk-input:focus, .uk-select:focus, .uk-textarea:focus { - border-color: #1D3557; } - -.scrollable-container { - max-height: 350px; - overflow-y: auto; - white-space: pre-wrap; - word-break: break-word; } - -/* target success notifications */ -.uk-notification-message-success { - background-color: #F1FAEE; - font-weight: 700; - color: #333333; - border-radius: 4px; } - -.uk-disabled, .disabled { - opacity: 0.5; } - -.ocr-notification { - background-color: #F1FAEE; - color: #333333; - border-radius: 4px; - padding: 0.5rem; } - -.rx-collection-link { - color: #1D3557 !important; - font-weight: 500; } - .rx-collection-link:hover { - color: #101e31 !important; } - -.rx-collection-separator { - margin: 0 0.35rem; - color: #595959; } - -.uk-modal-close-full { - margin: 25px 22px 0px 0px !important; - padding: 5px; } - .uk-modal-close-full:hover { - background-color: #e1f4da; } - -.uk-tab::before { - right: 240px; } - -/* .ecds-annotator sets color:white globally; reset text color for annotation - content popups and the Jodit editor which both have white backgrounds. */ -.rdx-annotation-content, -.jodit-container { - color: #333333; } - -.uk-search { - padding-right: 10px; } - -.rx-annotation-index-row { - display: flex; - align-items: center; - gap: 0.4rem; } - .rx-annotation-index-row a { - display: flex; - align-items: center; - line-height: 1; } - .rx-annotation-index-row .rx-label-copy { - vertical-align: middle; } - -.rx-search-result-list { - margin-top: 0.25rem !important; - margin-bottom: 0 !important; - padding-left: 1.25rem !important; } - .rx-search-result-list .rx-search-result-item { - margin-top: 0.2rem; } diff --git a/apps/static/css/components/search.css b/apps/static/css/components/search.css deleted file mode 100644 index 6c12cc50..00000000 --- a/apps/static/css/components/search.css +++ /dev/null @@ -1,181 +0,0 @@ -.uk-button-primary { - background-color: #1D3557; } - .uk-button-primary:hover { - background-color: #101e31; } - -/* -------------------------------------------------------------------------- */ -/* SEARCH RESULTS PAGE */ -/* -------------------------------------------------------------------------- */ -.info-line { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - margin-top: 2rem; - margin-bottom: 20px; - font-weight: 600; - color: #1D3557; } - .info-line .info-group, - .info-line .pagination-controls { - display: flex; - align-items: center; - gap: 5px; - flex-wrap: wrap; } - .info-line a, - .info-line select { - color: #1D3557; - text-decoration: underline; } - .info-line .pagination-controls .uk-icon-button { - color: #1D3557; } - .info-line .pagination-controls .uk-icon-button[disabled] { - color: #cccccc; - cursor: not-allowed; } - @media (max-width: 768px) { - .info-line { - flex-direction: column; - align-items: flex-start; } - .info-line .info-group, - .info-line .pagination-controls { - width: 100%; - justify-content: space-between; - margin-bottom: 10px; } - .info-line .info-group { - gap: 10px; } } -fieldset { - margin: inherit; - border: 0; - padding: inherit; } - -form#search-form input[type="search"][name="q"] { - width: 100%; } - -form#search-form .uk-button-danger { - background-color: #f0506e; - color: #ffffff; - border: 1px solid transparent; } - -form#search-form .uk-button-secondary { - background-color: #333333; - color: #ffffff; - border: 1px solid transparent; } - -#search-filters input[type="text"]#authors-filter { - width: 100%; } - -#search-filters select[multiple] { - height: 150px; - width: 100%; - overflow-y: scroll; - overflow-x: auto; } - -#search-filters .noUi-target { - margin: 45px 21px 10px; } - -.uk-container ul { - list-style: none; } - -.uk-checkbox:checked, -.uk-checkbox:indeterminate, -.uk-radio:checked { - background-color: #1D3557 !important; } - -.uk-search-default .uk-search-input:focus { - border-color: #1D3557; - border-right: 0; } - -.uk-input:focus, -.uk-select:focus, -.uk-textarea:focus { - border-color: #1D3557; } - -#search-grid { - margin-left: 0; - gap: 1.5rem; } - -.selectize-control.multi .selectize-input > div { - background: #1D3557 !important; - border: none !important; } - -.selectize-dropdown .active:not(.selected) { - background-color: #F1FAEE !important; } - -.selectize-control.plugin-clear_button .clear { - height: 85%; - top: -3px !important; } - -.noUi-connect { - background: #1D3557 !important; } - [disabled] .noUi-connect { - background: #1D3557 !important; - opacity: 0.2; } - -.noUi-tooltip { - font-size: .875rem; - font-family: monospace; - font-weight: normal; - padding: 0.125rem 0.25rem !important; - background-color: none; - border: none !important; } - -.sui-item-heading { - letter-spacing: 1.5px; - text-transform: uppercase; - font-weight: 600; - font-family: -apple-system, 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', sans-serif; - font-size: 13px; - margin-top: 0.65rem; } - .sui-item-heading a { - color: #1D3557; } - -#search-results { - list-style: none; } - #search-results dl { - margin-left: 2rem; } - #search-results .result-volume-summary { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - overflow: hidden; } - #search-results em { - color: #E60000 !important; - font-weight: bold; - font-style: normal; } - #search-results .result-page { - background-color: #F1FAEE; - color: #1D3557; } - #search-results .result-page a { - display: flex; - flex-flow: row nowrap; - justify-content: flex-start; - align-items: flex-start; - gap: 1rem; - margin: 0.5rem 0; - font-size: 0.8rem; - color: #1D3557; } - #search-results .result-page .page-number { - min-width: 4rem; - max-width: 4rem; - font-size: large; - font-weight: 600; - letter-spacing: -1px; - text-transform: uppercase; } - #search-results .result-page ul li { - list-style: disc; } - -.result-title { - color: #1D3557; } - .result-title:hover { - color: #101e31; } - -.search-button { - background-color: #E60000; - color: #ffffff; - padding: 4px 9px 0 9px; - border: none; - height: 40px; - font-size: 1.25rem; - transition: ease 0.1s; } - .search-button:hover { - background-color: #b30000; } - .search-button:active { - background-color: #670000; } diff --git a/apps/static/css/components/search.scss b/apps/static/css/components/search.scss index f2cd94ab..2a85b6e2 100644 --- a/apps/static/css/components/search.scss +++ b/apps/static/css/components/search.scss @@ -96,7 +96,100 @@ form#search-form { overflow-x: auto; } - .noUi-target { margin: 45px 21px 10px; } +} + +#date-range-filter { + margin-top: 0.5rem; + + .date-range-filter-fields { + display: flex; + gap: 0.75rem; + } + + .date-range-filter-field { + flex: 1; + min-width: 0; // let the selectize control shrink below its content width + + // selectize's own ".disabled" class barely changes the control's look — + // make it unmistakably inactive when the date filter is switched off. + .selectize-input.disabled { + opacity: 0.45; + cursor: not-allowed; + background: #F5F5F5; + } + } + + .date-range-filter-label { + display: block; + font-size: .75rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + color: $rx-color-dark-charcoal; + margin-bottom: 0.25rem; + transition: color 0.15s ease, opacity 0.15s ease; + + &.is-disabled { + color: $rx-color-dark-charcoal; + opacity: 0.4; + } + } + + .date-range-reset-link { + display: block; + background: none; + border: none; + padding: 0; + margin-top: 0.4rem; + font-size: .75rem; + color: $rx-color-midnight-blue; + text-decoration: underline; + cursor: pointer; + + &:hover { + opacity: 0.8; + } + } + + .date-range-bce-note { + margin: 0.5rem 0 0; + padding: 0.5rem 0.6rem; + font-size: .7rem; + line-height: 1.4; + color: $rx-color-dark-charcoal; + background: $rx-color-faded-mint; + border-radius: 3px; + transition: opacity 0.15s ease; + + &.is-disabled { + opacity: 0.4; + } + } + + .date-range-undated-toggle { + margin-top: 0.6rem; + margin-bottom: 0; + + label { + margin-bottom: 0; + text-transform: none; + font-weight: normal; + letter-spacing: normal; + font-size: .8rem; + } + + input[type="checkbox"]:disabled { + cursor: not-allowed; + opacity: 0.35; + } + } + + .date-range-undated-count { + display: block; + font-size: .7rem; + color: $rx-color-dark-charcoal; + opacity: 0.7; + } } // ————————————————————————————————————————————— @@ -148,27 +241,6 @@ form#search-form { top: -3px !important; } -// ————————————————————————————————————————————— -// noUi Slider (date slider) -// ————————————————————————————————————————————— -.noUi-connect { - background: $rx-color-midnight-blue !important; - - [disabled] & { - background: $rx-color-midnight-blue !important; - opacity: 0.2; - } -} - -.noUi-tooltip { - font-size: .875rem; - font-family: monospace; - font-weight: normal; - padding: 0.125rem 0.25rem !important; - background-color: none; - border: none !important; -} - // ————————————————————————————————————————————— // Typography // ————————————————————————————————————————————— diff --git a/apps/static/css/components/user-form.css b/apps/static/css/components/user-form.css deleted file mode 100644 index d620df2a..00000000 --- a/apps/static/css/components/user-form.css +++ /dev/null @@ -1,14 +0,0 @@ -.uk-button-primary { - background-color: #1D3557; } - .uk-button-primary:hover { - background-color: #101e31; } - -.unverified { - background-color: #f0506e; - color: white; - margin: 1rem; - padding: 0.25rem; - border-radius: 8px; } - -.uk-alert-info { - background-color: #F1FAEE; } diff --git a/apps/static/css/readux.css b/apps/static/css/readux.css deleted file mode 100644 index 001f9429..00000000 --- a/apps/static/css/readux.css +++ /dev/null @@ -1,1079 +0,0 @@ -/* -------------------------------------------------------------------------- */ -/* 0) IMPORTS */ -/* -------------------------------------------------------------------------- */ -@import url(./ecds-annotator.min.css); -/* ========================================================================== */ -/* RESPONSIVE MEDIA QUERIES */ -/* ========================================================================== */ -@media (max-width: 639px) { - .uk-logo { - padding: 0; } } - -@media (min-width: 640px) { - .rx-splash { - display: flex; - flex-direction: column; - align-self: flex-start; - position: sticky; - top: 0; - justify-content: space-between; - height: calc(100vh - 200px); } } - -@media (min-width: 768px) { - .modal-dialog > .modal-content { - top: 200px; } } - -@media (min-width: 960px) { - .uk-navbar-right { - flex-wrap: initial; } } - -@media (min-width: 320px) and (max-width: 959px) { - .rx-title-image { - object-fit: cover; - height: 150px; - width: 100%; } - footer { - padding: 2rem 0 0 0 !important; } - .content { - padding: 0 !important; } } - -@media only screen and (max-width: 480px) { - #box { - width: 100%; - padding-bottom: 100%; } } - -@media only screen and (max-width: 650px) and (min-width: 481px) { - #box { - width: 50%; - padding-bottom: 50%; } } - -@media only screen and (max-width: 1050px) and (min-width: 651px) { - #box { - width: 33.3%; - padding-bottom: 33.3%; } } - -@media only screen and (max-width: 1290px) and (min-width: 1051px) { - #box { - width: 25%; - padding-bottom: 25%; } } - -@media only screen and (max-width: 1250px) { - ol { - columns: 1; - -webkit-columns: 1; - -moz-columns: 1; } } - -/* Social Auth Buttons */ -a.rdx-provider-button { - color: #ffffff; - padding: 1rem; } - -a.rdx-provider-button:hover { - color: #cccccc; } - -.rdx-indented-help-block { - text-indent: 1.25rem; } - -/* End of Social Auth Buttons */ -/* Wagtail embedded objects */ -.rich-text img { - max-width: 100%; - height: auto; } - -.responsive-object { - position: relative; } - -.responsive-object iframe, -.responsive-object object, -.responsive-object embed { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; } - -/* end of Wagtail embedded objects */ -.uk-switch { - position: relative; - display: inline-block; - height: 17px; - width: 30px; } - -/* Hide default HTML checkbox */ -.uk-switch input { - display: none; } - -/* Slider */ -.uk-switch-slider { - background-color: #cccccc; - position: absolute; - top: 0; - left: 0; - right: 0; - border-radius: 500px; - bottom: 0; - cursor: pointer; - transition-property: background-color; - transition-duration: .2s; } - -/* Switch pointer */ -.uk-switch-slider:before { - content: ''; - background-color: #ffffff; - position: absolute; - width: 15px; - height: 15px; - left: 1px; - bottom: 1px; - border-radius: 50%; - transition-property: transform, box-shadow; - transition-duration: .2s; } - -/* Slider active color */ -input:checked + .uk-switch-slider { - background-color: #1D3557 !important; } - -/* Pointer active animation */ -input:checked + .uk-switch-slider:before { - transform: translateX(13px); } - -input:checked + .uk-switch-slider.uk-switch-big:before { - transform: translateX(13px) scale(1.2); } - -/* -------------------------------------------------------------------------- */ -/* FOOTER */ -/* -------------------------------------------------------------------------- */ -.footer { - background-color: #457B9D; - color: #ffffff; - padding: 20px; } - -.footer-links { - display: flex; - flex-direction: column; - font-weight: bold; } - -.footer-links a { - color: #ffffff; - text-decoration: none; } - .footer-links a:hover { - text-decoration: underline; } - -.footer-logo { - display: flex; - align-items: center; } - .footer-logo svg { - width: 50px; - height: 50px; } - -.footer-bottom { - text-align: left; - margin-top: 10px; - color: #ffffff; } - -.uk-button-primary { - background-color: #1D3557; } - .uk-button-primary:hover { - background-color: #101e31; } - -.unverified { - background-color: #f0506e; - color: white; - margin: 1rem; - padding: 0.25rem; - border-radius: 8px; } - -.uk-alert-info { - background-color: #F1FAEE; } - -/* -------------------------------------------------------------------------- */ -/* 1) ROOT VARIABLES */ -/* -------------------------------------------------------------------------- */ -:root { - --contrast: 200%; - --link-color: $rx-color-midnight-blue; } - -/* -------------------------------------------------------------------------- */ -/* 2) BASE / LAYOUT */ -/* -------------------------------------------------------------------------- */ -.sr-only { - position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; - clip: rect(0 0 0 0) !important; - clip-path: inset(50%) !important; - border: 0 !important; - white-space: nowrap !important; } - -html, body { - height: 100%; - margin: 0; - display: flex; - flex-direction: column; } - -/* #v-readux is the direct child of body; it must stretch and pass height down to main */ -#v-readux { - flex: 1; - display: flex; - flex-direction: column; } - -main { - flex-grow: 1; } - -/* Home page navigation transparent, light text/icons over dark hero */ -.home-nav nav { - background-color: transparent !important; } - -.home-nav a, .home-nav .brand-logo, .home-nav .brand-tagline { - color: #ffffff !important; } - -.home-nav svg line, .home-nav svg path, .home-nav svg circle, .home-nav svg polygon, .home-nav svg polyline, .home-nav svg rect { - stroke: #ffffff !important; } - -/* Overlay & content shell */ -.overlay { - position: absolute; - inset: 0 auto auto 0; - width: 100%; - height: 80vh; - min-height: 500px; - background-color: rgba(29, 53, 87, 0.8); } - -.content { - position: relative; - z-index: 2; - min-height: 100vh; - display: flex; - flex-flow: column; } - .content .space { - flex-grow: 1; } - .content .uk-section-default:empty { - background-color: transparent; } - -/* Text & headings */ -.paragraph { - color: #1D3557; - line-height: normal; } - -.title { - color: #1D3557; - font-size: x-large; - font-weight: bold; } - -.hero { - padding-top: 15vh; - color: #ffffff; } - -.uk-container h2 { - margin-bottom: 0.35rem; } - -/* Utilities */ -.slash-gap { - margin: 0 0.5rem; } - -.color-black { - color: #000000; } - -/* Make nav z-order above content when overlayed */ -.uk-navbar-container { - z-index: 10; - position: absolute; - width: 100%; } - -/* -------------------------------------------------------------------------- */ -/* 3) GLOBAL LINKS & INTERACTIVE */ -/* -------------------------------------------------------------------------- */ -.uk-link, a { - color: #1D3557; - transition: color 0.1s ease; } - .uk-link:hover, a:hover { - color: rgba(29, 53, 87, 0.8); } - -.text-anchor { - color: #E60000; - font-weight: 600; - transition: color 0.1s ease; - text-decoration: underline; } - .text-anchor:hover { - color: #b30000; } - .text-anchor:active { - color: #670000; } - -.text-anchor-blue { - color: #1D3557; - font-weight: 700; - transition: color 0.1s ease; } - .text-anchor-blue:hover { - color: #101e31; - text-decoration: underline; } - .text-anchor-blue:active { - color: black; - text-decoration: underline; } - -/* Consolidated interactive text on dark backgrounds */ -.brand-logo, .menu-item, .brand-readux { - color: #ffffff !important; - transition: color 0.1s ease; - text-decoration: none !important; } - .brand-logo:hover, .menu-item:hover, .brand-readux:hover { - color: #e6e6e6 !important; } - .brand-logo:active, .menu-item:active, .brand-readux:active { - color: #bfbfbf !important; } - -.brand-logo { - font-size: large; - font-weight: bold; } - -.menu-item { - text-transform: unset !important; } - -.brand-inline { - font-size: small; - font-weight: normal; - text-decoration: underline !important; } - -.brand-tagline { - font-size: small; - color: #ffffff; } - -/* Page titles */ -.page-title { - font-size: xx-large; - font-weight: bold; - color: #000000; } - -.page-text-lead { - font-size: medium; - color: #000000; } - -em { - color: inherit !important; } - -/* -------------------------------------------------------------------------- */ -/* 4) BREADCRUMBS & DROPDOWNS */ -/* -------------------------------------------------------------------------- */ -.breadcrumb { - list-style: none; - display: flex; - font-family: -apple-system, 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', sans-serif; - font-size: 14px; - padding: 0; - margin: 0; } - .breadcrumb > li { - margin-right: 5px; } - .breadcrumb > li:last-child:empty { - display: none; } - .breadcrumb > li + li::before { - content: "/"; - margin-right: 5px; - color: #1D3557; } - .breadcrumb > li:last-child:empty + li::before { - display: none; } - .breadcrumb a { - color: #1D3557; - transition: color 0.1s ease; } - .breadcrumb a:hover { - color: rgba(29, 53, 87, 0.8); } - .breadcrumb .icon-chevron-down { - margin-left: 3px; - top: 1px; - position: relative; } - -/* Breadcrumb dropdown */ -.uk-navbar-dropdown { - background-color: #F1FAEE; - border-radius: 4px; - box-shadow: 0 4px 4px rgba(0, 0, 0, 0.1); - padding: 10px 0 !important; - min-width: 0 !important; } - -.uk-navbar-dropdown-nav a, -.uk-navbar-dropdown-nav.sort-dropdown label { - cursor: pointer; - color: #1D3557 !important; - font-weight: 600; - padding: 5px 15px !important; - display: block; - transition: background-color 0.1s ease; } - .uk-navbar-dropdown-nav a:hover, - .uk-navbar-dropdown-nav.sort-dropdown label:hover { - background-color: rgba(29, 53, 87, 0.1); } - -.uk-navbar-dropdown-nav.sort-dropdown label input { - display: none; } - -.pagination-nav span { - padding: 5px 15px !important; - display: block; } - -/* -------------------------------------------------------------------------- */ -/* 5) UIKIT OVERRIDES / COMPONENTS */ -/* -------------------------------------------------------------------------- */ -/* Slidenav */ -.uk-slidenav { - color: rgba(29, 53, 87, 0.6); } - .uk-slidenav:hover { - color: #1D3557; } - -/* Focus ring */ -.uk-input:focus, .uk-select:focus, .uk-textarea:focus { - border-color: #1D3557; } - -/* Buttons */ -.uk-button-default { - transition: border-color 0.1s ease; } - .uk-button-default:hover { - border-color: #101e31; } - .uk-button-default:active { - border-color: black; } - -.uk-button-primary { - background-color: var(--link-color) !important; - color: #fff !important; } - .uk-button-primary:hover, .uk-button-primary:active { - background-color: var(--link-color) !important; - color: #fff !important; - filter: contrast(var(--contrast)); } - -/* Close icon */ -.uk-close { - color: #1D3557; } - .uk-close:hover { - color: #101e31; } - -/* Slider */ -.uk-slider-items { - gap: 4rem; } - -/* Tabs */ -.uk-tab > .uk-active > a { - border-color: var(--link-color) !important; - color: var(--link-color) !important; } - -/* Form controls */ -.uk-checkbox, .uk-radio { - border: 1px solid #cccccc !important; } - -/* Lists & list items */ -.uk-container ul { - list-style: none; - padding-left: 0; - margin-left: 0; - list-style: none; } - -/* Navbar */ -.uk-navbar-right { - flex-wrap: initial; } - -/* Offcanvas */ -.uk-offcanvas-container { - top: -80px; - position: inherit; } - -/* Accordion */ -.uk-accordion-content { - color: #595959 !important; } - -.uk-accordion > :nth-child(n+2) { - margin-top: 10px; } - -/* Override UIkit accordion icons */ -.uk-open > .uk-accordion-title::before { - background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%231D3557%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%20%2F%3E%0A%3C%2Fsvg%3E); } - -.uk-open > .uk-accordion-title:hover::before { - background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%231D3557%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%20%2F%3E%0A%3C%2Fsvg%3E); } - -.uk-accordion-title::before { - background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%231D3557%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%20%2F%3E%0A%20%20%20%20%3Crect%20fill%3D%22%231D3557%22%20width%3D%221%22%20height%3D%2213%22%20x%3D%226%22%20y%3D%220%22%20%2F%3E%0A%3C%2Fsvg%3E); } - -.uk-accordion-title:hover::before { - background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%231D3557%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%20%2F%3E%0A%20%20%20%20%3Crect%20fill%3D%22%231D3557%22%20width%3D%221%22%20height%3D%2213%22%20x%3D%226%22%20y%3D%220%22%20%2F%3E%0A%3C%2Fsvg%3E); } - -/* Offcanvas accordion color override */ -.uk-offcanvas-bar .uk-open > .uk-accordion-title::before { - background-image: url("data:image/svg+xml;charset=UTF-8,%8Csvg+width%3D%2213%22+height%3D%2213%22+viewBox%3D%220+0+13+13%22+xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0D%0A++++%3Crect+fill%3D%22rgba%28149%2C+9%2C+83%2C+1%29%22+width%3D%2213%22+height%3D%221%22+x%3D%220%22+y%3D%226%22+%2F%3E%0D%0A%3C%2Fsvg%3E") !important; } - -/* -------------------------------------------------------------------------- */ -/* 6) CARDS / GRID / LISTS */ -/* -------------------------------------------------------------------------- */ -/* Volume grid */ -.volume-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 2.5rem; } - -.grid-item { - text-align: left; - background-color: transparent; - cursor: pointer; - text-decoration: none; - opacity: 0.75; - transition: opacity 0.1s ease; - display: block; - color: #000000; } - .grid-item:hover { - opacity: 1; - text-decoration: none; - color: #000000; } - .grid-item img { - width: 100%; - height: auto; - display: block; - border-radius: 4px; } - .grid-item h2 { - text-decoration: underline; - color: #1D3557; - font-weight: 600; - font-size: medium; - margin: 0; } - .grid-item p { - margin: 0; } - -.collection-summary { - display: -webkit-box; - -webkit-line-clamp: 4; - line-clamp: 4; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - white-space: normal; } - -/* List view table */ -.list-view-table { - margin-left: -0.5rem; } - .list-view-table thead th { - color: #1D3557; - font-weight: bold; } - .list-view-table tbody tr { - color: rgba(0, 0, 0, 0.75); - transition: color 0.3s ease; } - .list-view-table tbody tr:hover { - background-color: #F1FAEE !important; - color: #000000; } - .list-view-table th, .list-view-table td { - padding: 0.5rem; } - -/* Info line / pagination */ -.info-line { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 2rem; - margin-bottom: 20px; - font-weight: 600; - color: #1D3557; } - .info-line .info-group, .info-line .pagination-controls { - display: flex; - align-items: center; - gap: 5px; } - .info-line a, .info-line select { - color: #E60000; - text-decoration: underline; } - -.pagination-controls .uk-icon-button { - color: #1D3557; } - -.pagination-controls .uk-icon-button[disabled] { - color: #cccccc; - cursor: not-allowed; } - -/* Hoverable rows */ -.list-item:hover, .clickable-row:hover { - cursor: pointer; } - -/* -------------------------------------------------------------------------- */ -/* 7) SEARCH */ -/* -------------------------------------------------------------------------- */ -.search-bar form { - display: flex; - align-items: center; - width: 100%; - max-width: 800px; - margin: 2rem auto; } - -.search-bar input { - flex: 1; - height: 50px; - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; - font-size: 1.2em; } - -.search-bar button { - height: 50px; - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; - font-size: 1.2em; - background-color: #E60000; - color: #ffffff; - border: none; - display: flex; - align-items: center; - justify-content: center; - transition: background-color 0.3s ease; } - .search-bar button:hover, .search-bar button:active { - background-color: #b30000; } - -/* Featured text area */ -.truncate-text { - display: -webkit-box; - -webkit-line-clamp: 5; - line-clamp: 5; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - white-space: normal; } - -.section-offset { - background-color: #ffffff; - margin-top: 200px; } - -/* Video section */ -.video-section { - position: relative; - text-align: center; - color: #ffffff; } - -.video-overlay { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 100%; - cursor: pointer; - z-index: 2; } - .video-overlay h2 { - margin-bottom: 10px; - font-size: 2em; } - .video-overlay .play-icon { - font-size: 48px; - margin-bottom: 10px; } - .video-overlay p { - max-width: 600px; - margin: 0 auto; } - -.video-container { - position: relative; - width: 100%; - padding-bottom: 56.25%; - background: #000000; } - -.video-thumbnail, iframe, .transparent-overlay { - position: absolute; - inset: 0; - width: 100%; - height: 100%; } - -.video-thumbnail { - object-fit: cover; } - -.transparent-overlay { - background-color: rgba(0, 0, 0, 0.5); - z-index: 1; } - -/* -------------------------------------------------------------------------- */ -/* 10) SECTIONS / HIGHLIGHTS */ -/* -------------------------------------------------------------------------- */ -.column { - background-color: #F1FAEE; - padding: 2rem; - border-radius: 8px; - text-align: center; - position: relative; } - -/* -------------------------------------------------------------------------- */ -/* 11) BUTTONS / AUTH */ -/* -------------------------------------------------------------------------- */ -.description-button { - background-color: #E60000 !important; - transition: background-color 0.1s ease; } - .description-button:hover { - background-color: #b30000 !important; } - .description-button:active { - background-color: #670000 !important; } - -button.google, button.github { - color: #ffffff; } - -/* -------------------------------------------------------------------------- */ -/* 12) CONTENT PADDING TWEAKS */ -/* -------------------------------------------------------------------------- */ -.content > .uk-section { - padding-bottom: 0; } - -/* -------------------------------------------------------------------------- */ -/* 13) PROJECT-SPECIFIC ELEMENTS (rx-*) */ -/* -------------------------------------------------------------------------- */ -.rx-breadcrumb, .rx-action-btn { - color: var(--link-color); - text-transform: uppercase; - font-weight: bold; - display: inline-flex; - padding: 0; - margin: 0; - user-select: auto; } - -.rx-action-btn { - letter-spacing: 0.1em; - font-size: small; } - -.rx-breadcrumb-item > a, .rx-action-btn, .rx-icon-btn, .uk-dropdown-nav > li > a:hover { - color: var(--link-color) !important; - opacity: 0.75; - transition: all 100ms ease-in-out; } - -.rx-breadcrumb-item > a:hover, .rx-action-btn:hover, .rx-icon-btn:hover, .uk-dropdown-nav > li.uk-active > a { - color: var(--link-color); - opacity: 1; - transition: all 100ms ease-in-out; } - -.rx-anchor { - color: var(--link-color) !important; - opacity: 0.75; - word-break: break-all; - transition: all 100ms; } - .rx-anchor:hover { - color: var(--link-color); - opacity: 1; } - -.rx-label-copy { - background: #1D3557 !important; - color: #ffffff !important; - border: none !important; - font-size: 0.75rem; - letter-spacing: 0.1rem; - cursor: pointer; - transition: background 150ms ease-in-out, opacity 150ms ease-in-out; - font-weight: 600; - font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace !important; } - .rx-label-copy:hover { - background: #0e1929 !important; - color: #ffffff !important; - opacity: 0.9; } - .rx-label-copy:active { - background: #04060b !important; - opacity: 1; } - -/* Navigation & bars */ -.breadcrumbs { - font-size: 80%; - margin: 0; } - -/* Media / imagery */ -.openseadragon-container span { - color: transparent; - display: inline-flex !important; - font-size: 100%; - line-height: initial; - white-space: nowrap; } - -.box { - width: 25%; - padding-bottom: 25%; - position: relative; - float: left; } - -.box a img { - width: 100%; - overflow: hidden; } - -ul.listing-thumbs > li > img.thumbnail-image { - height: 150px; } - -/* Info panels */ -.rx-head-container { - margin: 0 0 3em; } - -.rx-info-content { - padding: 0 0 1.25em; - font-size: 0.9rem; } - .rx-info-content:last-child { - padding: 0; } - -.rx-info-content-label { - color: #595959 !important; - font-weight: 600 !important; - font-size: 0.9rem !important; } - -.rx-info-content-value { - color: #595959; - line-height: 1.5; } - -.rx-info-content .uk-tab a { - color: #595959; } - .rx-info-content .uk-tab a:hover { - color: #666; } - -/* Badges */ -.rx-annotation-badge { - border-radius: 3px; - background: #ffffff; - color: #595959 !important; - border: #595959 1px solid; - font-size: 0.75rem; - letter-spacing: 0.1rem; - font-weight: 600; - display: inline-block; - cursor: default; - text-transform: uppercase; - padding: 0.1rem 0.5rem; } - -/* Fieldsets */ -.rx-fieldset { - border: none; } - -/* Space between label and input */ -span.fieldname { - margin-right: 0.5rem; } - -/* Titles & banners */ -.rx-sticky { - position: sticky; - top: 0; - z-index: 1; } - -.rx-title-container { - background: #ffffff; - padding: 1em 0; - position: sticky; - top: 0; - z-index: 3; } - -.rx-title { - font-size: 2em; - font-weight: bold; - color: var(--link-color); } - -.rx-title-2 { - font-size: 1.5em; - font-weight: bold; - color: #595959; } - -.rx-title-tagline { - font-size: 14px; - color: #ffffff; } - -.rx-title-image { - object-fit: cover; - height: 150px; - width: 100%; } - -.rx-breadcrumb-item { - font-size: 1rem; - font-weight: bold; - color: var(--link-color); } - -/* Tooltip */ -.tooltip { - position: relative; - display: inline-block; - border-bottom: 1px dotted black; } - -.tooltip .tooltiptext { - visibility: hidden; - width: 120px; - background-color: #000000; - color: #ffffff; - text-align: center; - border-radius: 6px; - padding: 5px 0; - position: absolute; - z-index: 1; - top: 150%; - left: 50%; - margin-left: -60px; - font-size: 14px; } - -.tooltip .tooltiptext::after { - content: ""; - position: absolute; - bottom: 100%; - left: 50%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent black transparent; } - -.tooltip:hover .tooltiptext { - visibility: visible; } - -/* Page search */ -.rx-page-search-container { - width: calc(100% - 30px) !important; - color: #595959 !important; } - -.rx-page-search-option-label { - display: block; } - -/* Reader / viewer & layout utils */ -.rdx-viewer-container { - position: absolute; - left: 0; - bottom: 0; - height: calc(100% - 36px); - width: 100%; - margin: 0; } - -#rdx-viewer > div > div > div.py-8.grid.grid-cols-2.gap-2 { - padding-bottom: 0; } - -.reader-modal { - width: 400px !important; - height: fit-content !important; - padding: 0 !important; - left: 75px !important; - top: 60px !important; - background: #ffffff; } - -.rx-line-height-sm { - line-height: 1.25em; } - -.rx-volume-search em { - color: #E60000 !important; } - -.rx-volume-search { - max-height: 75vh; } - -.rx-padding-extra-small { - padding: 10px; } - -.rx-scrollable-area { - max-height: 75vh; - overflow-y: auto; - -ms-overflow-style: none; - scrollbar-width: none; } - .rx-scrollable-area::-webkit-scrollbar { - display: none; } - -.count { - position: absolute; - top: 0; - right: 0; - padding-top: 20%; - padding-right: 20%; - height: 10px; - width: 10px; - font-size: 10px; - text-align: center; } - -.rx-flex { - display: flex; } - -.uk-accordion-content { - padding: 0.5rem 10px; } - -/* Accordion (rx-) */ -.rx-accordion-head { - background: #ffffff; - color: var(--link-color); - border: var(--link-color) 1px solid; - font-size: 0.85rem; - letter-spacing: 0.1rem; - cursor: pointer; - transition: all 100ms ease-in-out; - font-weight: 600; - font-family: 'Consolas','Monaco','Andale Mono','Ubuntu Mono','monospace' !important; } - .rx-accordion-head::before { - color: #ffffff; } - -.rx-accordion-handle { - color: var(--link-color) !important; } - -.rx-accordion-content { - margin: 0; - padding: 0 0 10px 0; } - -.rx-accordion-container { - margin: 20px 0 0 0; } - -.thumbnail-container { - width: 120px; } - -.thumbnail-container img { - width: 100%; - height: auto; - object-fit: cover; } - -.cursor-default:hover { - cursor: default !important; } - -/* Volume grid specific styles */ -/* square container for covers */ -.cover-square { - position: relative; - width: 100%; - aspect-ratio: 1 / 1; - /* makes a perfect square */ - background: #000; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - border-radius: 4px; } - -.cover-square img { - max-width: 100%; - max-height: 100%; - width: auto; - height: auto; - display: block; - object-fit: contain; - object-position: center; } - -.volume-title, -.volume-title-inline { - margin-top: .5rem; - margin-bottom: .25rem; - line-height: 1.25; } - -.volume-title__link { - color: #1D3557; - font-weight: 600; - font-size: medium; - text-decoration: underline; } - -.volume-title__year { - white-space: nowrap; } - -.volume-title__link:hover { - color: #1D3557; - text-decoration: underline; } - -.volume-year { - margin-top: .5rem; - font-weight: 600; - color: #1D3557; - line-height: 1.25; } - -.author-line { - color: #555; - line-height: 1.25; - display: -webkit-box; - -webkit-line-clamp: 3; - line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - white-space: normal; } - -/* End */ -.uk-notification-message { - background-color: #F1FAEE; - color: #1D3557; - border-radius: 4px; - box-shadow: 0 4px 12px rgba(29, 53, 87, 0.15); } - -.rx-line-height-lg { - line-height: 1.15em; } - -.rx-line-height-md { - line-height: 1em; } - -.rx-line-height-sm { - line-height: 0.85em; } - -.theme-toggle { - display: none; } diff --git a/apps/static/css/readux.scss b/apps/static/css/readux.scss index bee4ac9c..f27a845b 100644 --- a/apps/static/css/readux.scss +++ b/apps/static/css/readux.scss @@ -16,12 +16,18 @@ /* -------------------------------------------------------------------------- */ :root { --contrast: 200%; - --link-color: $rx-color-midnight-blue; + --link-color: #{$rx-color-midnight-blue}; } /* -------------------------------------------------------------------------- */ /* 2) BASE / LAYOUT */ /* -------------------------------------------------------------------------- */ +// Hides the raw, un-mounted Vue markup (custom component tags, [[ ]] +// interpolations) that briefly flashes on page load before Vue finishes +// mounting on #v-readux and removes this attribute. Must be a plain CSS +// rule (not conditional on JS) so it applies during the very first paint. +[v-cloak] { display: none; } + .sr-only { @include sr-only(); } html, body { @@ -292,6 +298,10 @@ em { color: inherit !important; } font-weight: 600; color: $rx-color-midnight-blue; .info-group, .pagination-controls { display: flex; align-items: center; gap: 5px; } a, select { color: $rx-color-mario-red; text-decoration: underline; } + // UIkit's uk-dropdown strips the whitespace text node between this label + // and its dropdown toggle link when it enhances the toggle client-side, + // so the source's natural whitespace can't be relied on for spacing here. + .fieldname { margin-right: 0.25rem; } } .pagination-controls { .uk-icon-button { color: $rx-color-midnight-blue; } .uk-icon-button[disabled] { color: $rx-color-light-pearl; cursor: not-allowed; } } diff --git a/apps/static/js/components/OcrInspector.vue b/apps/static/js/components/OcrInspector.vue index 88224823..a35b4fcb 100644 --- a/apps/static/js/components/OcrInspector.vue +++ b/apps/static/js/components/OcrInspector.vue @@ -22,7 +22,7 @@
- Superimpose OCR text as a layer on top of the scanned volume image. We try our best to align the text to image but some may miss. + Superimpose OCR text as a layer on top of the scanned volume image. We try our best to align the text to the image but some text may be missing or out of place.
diff --git a/apps/static/js/index.js b/apps/static/js/index.js index 552d110d..7dc4ca1c 100644 --- a/apps/static/js/index.js +++ b/apps/static/js/index.js @@ -1,6 +1,5 @@ import $ from 'jquery' import axios from 'axios' -import noUiSlider from 'nouislider' import '@selectize/selectize' import UIkit from 'uikit' import UIkitIcons from 'uikit/dist/js/uikit-icons' @@ -11,7 +10,6 @@ import './vue-readux.js' window.$ = $ window.jQuery = $ window.axios = axios -window.noUiSlider = noUiSlider UIkit.use(UIkitIcons) window.UIkit = UIkit window.UIkitIcons = UIkitIcons diff --git a/apps/static/js/search.js b/apps/static/js/search.js index 959a386d..86515441 100644 --- a/apps/static/js/search.js +++ b/apps/static/js/search.js @@ -7,7 +7,12 @@ let displayElement; let relevanceSortOption; let defaultSortOption; let form; -let slider; +let startYearSelect; +let endYearSelect; +let includeUndatedCheckbox; +let resetDateRangeButton; +let fullMinYear; +let fullMaxYear; let resetFiltersButton; let allFilters; let dateToggleSwitch; @@ -17,7 +22,6 @@ let queryMinYear; let queryMaxYear; let authorsFilter; let authorsMultiselect; -let dateRange; window.addEventListener("DOMContentLoaded", () => { // Get URL params @@ -35,7 +39,10 @@ window.addEventListener("DOMContentLoaded", () => { // initialize elements form = document.querySelector("form#search-form"); dateToggleSwitch = document.querySelector("input[type='checkbox']#toggle-date"); - dateRange = document.querySelector(".noUi-tooltip"); + startYearSelect = document.getElementById("id_start_year"); + endYearSelect = document.getElementById("id_end_year"); + includeUndatedCheckbox = document.getElementById("id_include_undated"); + resetDateRangeButton = document.getElementById("reset-date-range"); sortElement = document.querySelector("select#id_sort"); displayElement = document.querySelector("select#id_display"); relevanceSortOption = sortElement.querySelector("option[value='_score']"); @@ -59,60 +66,137 @@ window.addEventListener("DOMContentLoaded", () => { // Attach event listener to filter author multiselect options // authorsFilter.addEventListener("input", handleAuthorsFilter); - // Set up slider - slider = document.getElementById("date-range-slider"); - setUpSlider(slider); + // Set up start/end year dropdowns + setUpYearDropdowns(); - // Initialize date toggle switch and add event listener + // "Show volumes without a published date" is a standing preference, not a + // one-off filter: once someone turns it on they almost always want it on + // for every future search, so it's stuck in localStorage rather than reset + // whenever filters are cleared or a new search is run. An explicit + // include_undated=on in the URL (e.g. a shared link) still wins and keeps + // localStorage in sync. + const urlHasIncludeUndated = Boolean(urlParams) && urlParams.get("include_undated") === "on"; + const storedIncludeUndated = localStorage.getItem("readux:includeUndated") === "true"; + includeUndatedCheckbox.checked = urlHasIncludeUndated || storedIncludeUndated; + includeUndatedCheckbox.addEventListener("change", () => { + localStorage.setItem("readux:includeUndated", includeUndatedCheckbox.checked); + }); + + resetDateRangeButton.addEventListener("click", resetDateRange); + + // Initialize date toggle switch and add event listener. The checkbox + // itself isn't a bound Django form field, so its checked state has to be + // synced here explicitly — otherwise a URL carrying start_date/end_date + // (e.g. from a shared link) leaves the controls enabled while the + // checkbox still visually reads "off". + dateToggleSwitch.checked = dateToggleState; setDateFieldToggleState(dateToggleState); dateToggleSwitch.addEventListener("change", toggleDate); - + // Add reset filters event listener allFilters = document.querySelectorAll("#search-filters select"); document.querySelectorAll("button.reset-filters").forEach(button => { button.addEventListener("click", resetFilters); }); - // Attach event listeners to form to handle slider input - form.addEventListener("submit", handleSubmit); - form.addEventListener("formdata", handleFormData); }); -function setUpSlider(slider) { - // Prepare the date range slider based on available data +function setUpYearDropdowns() { + // Prepare the start/end year dropdowns based on available data - // Get min and max from data attribute on Django form inputs - const minDate = document.querySelector("input[name='start_date']").getAttribute("data-date-initial"); - const maxDate = document.querySelector("input[name='end_date']").getAttribute("data-date-initial"); - // Get year for slider purposes - let minYear = parseInt(minDate.split("-")[0]); - let maxYear = parseInt(maxDate.split("-")[0]); + // Get min and max from data attributes set from Elasticsearch aggregations + const container = document.getElementById("date-range-filter"); + const minDate = container.getAttribute("data-min-date"); + const maxDate = container.getAttribute("data-max-date"); + let minYear = minDate ? parseInt(minDate.split("-")[0]) : 0; + let maxYear = maxDate ? parseInt(maxDate.split("-")[0]) : 0; // If there is no min and max (i.e. query returned 0 results), use query params for date if (!maxYear && !minYear && urlParams) { if (queryMinYear) minYear = parseInt(queryMinYear.split("-")[0]); if (queryMaxYear) maxYear = parseInt(queryMaxYear.split("-")[0]); } - - // setup noUISlider - noUiSlider.create(slider, { - // fallback to range [1, current year] if necessary - start: [minYear || 1, maxYear || new Date().getFullYear()], - connect: true, - range: { - "min": minYear || 1, - "max": maxYear || new Date().getFullYear() - }, - step: 1, - tooltips: true, - format: { - from: function(value) { - return parseInt(value); - }, - to: function(value) { - return parseInt(value); - } - }, + minYear = minYear || 1; + maxYear = maxYear || new Date().getFullYear(); + fullMinYear = minYear; + fullMaxYear = maxYear; + + const selectedStartYear = queryMinYear ? parseInt(queryMinYear.split("-")[0]) : minYear; + const selectedEndYear = queryMaxYear ? parseInt(queryMaxYear.split("-")[0]) : maxYear; + + // Volumes dated before year 1 CE get clamped to year 1 on the backend + // (there's no way to represent BCE with a plain JS/Python date), so make + // that clear on the option itself rather than silently showing "1". + const hasBce = container.getAttribute("data-has-bce") === "true"; + const minYearLabel = hasBce && minYear === 1 ? "1 or earlier (BCE)" : null; + + populateYearSelect(startYearSelect, minYear, maxYear, selectedStartYear, "01-01", minYearLabel); + populateYearSelect(endYearSelect, minYear, maxYear, selectedEndYear, "12-31"); + + // Turn the long year lists into type-to-filter dropdowns instead of + // plain scrolling selects, which get unwieldy over a multi-century range. + // Selectize propagates value changes via jQuery's synthetic "change" + // event, which native addEventListener listeners never see — so the + // start/end sync below has to be bound through jQuery too. + [startYearSelect, endYearSelect].forEach((select) => { + $(select).selectize({ + maxItems: 1, + allowEmptyOption: false, + }); + }); + + // Keep start year <= end year at all times + $(startYearSelect).on("change", () => { + const startYear = parseInt(startYearSelect.value); + if (startYear > parseInt(endYearSelect.value)) { + setYearSelectValue(endYearSelect, `${String(startYear).padStart(4, "0")}-12-31`); + } + updateResetDateRangeVisibility(); }); + $(endYearSelect).on("change", () => { + const endYear = parseInt(endYearSelect.value); + if (endYear < parseInt(startYearSelect.value)) { + setYearSelectValue(startYearSelect, `${String(endYear).padStart(4, "0")}-01-01`); + } + updateResetDateRangeVisibility(); + }); + + updateResetDateRangeVisibility(); +} + +function updateResetDateRangeVisibility() { + // Only offer the shortcut once the selection is actually narrower than + // the full available range + const isFullRange = + parseInt(startYearSelect.value) === fullMinYear && + parseInt(endYearSelect.value) === fullMaxYear; + resetDateRangeButton.hidden = isFullRange || !dateToggleState; +} + +function resetDateRange() { + // Restore the start/end dropdowns to the full available range + setYearSelectValue(startYearSelect, `${String(fullMinYear).padStart(4, "0")}-01-01`); + setYearSelectValue(endYearSelect, `${String(fullMaxYear).padStart(4, "0")}-12-31`); + form.submit(); +} + +function setYearSelectValue(select, value) { + // Update a year
-
- +
+
+
+ + +
+
+ + +
+
+

+ Volumes dated before year 1 CE (BCE) can't be shown individually here — they're grouped under the earliest year in the list above{% if date_range_has_bce %}, labeled "1 or earlier"{% endif %}. +

+ +
+ + +
+
{% for key in CUSTOM_METADATA_KEYS %} @@ -139,7 +164,7 @@
- +
diff --git a/apps/templates/snippets/_collection_summary.html b/apps/templates/snippets/_collection_summary.html new file mode 100644 index 00000000..5866b47e --- /dev/null +++ b/apps/templates/snippets/_collection_summary.html @@ -0,0 +1,17 @@ +{% comment %} +Renders a collection's truncated summary, with the full text available via +the native `title` attribute when truncation actually happens. + +Uses a plain `title` attribute rather than `uk-tooltip` here: UIkit's +`uk-tooltip` options string is split on `;`, so free-text descriptions +(which often contain semicolons) would get silently cut off mid-tooltip. + +Usage: {% include "snippets/_collection_summary.html" with collection=collection tag="p" css_class="collection-summary" %} +{% endcomment %} +{% with full_summary=collection.summary|striptags truncated_summary=collection.summary|striptags|truncatewords:70 %} + {% if truncated_summary != full_summary %} + <{{ tag }} class="{{ css_class }}" title="{{ full_summary }}">{{ truncated_summary|safe }} + {% else %} + <{{ tag }} class="{{ css_class }}">{{ truncated_summary|safe }} + {% endif %} +{% endwith %} diff --git a/package.json b/package.json index 7db0f97a..f745d4ff 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "axios": "^1.7.0", "ecds-annotator": "file:../ecds-annotator", "jquery": "^3.6.0", - "nouislider": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "vue": "^3.4.0"