diff --git a/src/assets/svg/icon-download.svg b/src/assets/svg/icon-download.svg new file mode 100644 index 0000000..5dbdaf4 --- /dev/null +++ b/src/assets/svg/icon-download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/js/components/table/sortable-table.js b/src/js/components/table/sortable-table.js new file mode 100644 index 0000000..df358e7 --- /dev/null +++ b/src/js/components/table/sortable-table.js @@ -0,0 +1,111 @@ +function getRowsContainer(root) { + if (root.tagName === "TABLE") { + return root.querySelector("tbody") || root; + } + const selector = root.getAttribute("data-sort-rows"); + if (selector) return root.querySelector(selector); + return ( + root.querySelector(".iati-file-card-table__cards") || + root.querySelector(".iati-file-card-table__rows") || + root + ); +} + +function getHeaders(root) { + return Array.from(root.querySelectorAll("[aria-sort]")); +} + +function getRows(rowsContainer) { + return Array.from(rowsContainer.children).filter( + (el) => + el.tagName !== "THEAD" && + !el.classList.contains("iati-file-card-table__header"), + ); +} + +function getCellText(row, columnIndex) { + const cells = row.querySelectorAll( + "td, .iati-file-card__cell, .iati-table__cell", + ); + const cell = cells[columnIndex]; + return cell ? cell.textContent.trim() : ""; +} + +function compareValues(a, b, type) { + if (type === "number") { + const numA = parseFloat(a.replace(/[^0-9.\-]/g, "")) || 0; + const numB = parseFloat(b.replace(/[^0-9.\-]/g, "")) || 0; + return numA - numB; + } + return a.localeCompare(b, undefined, { numeric: true }); +} + +function sortRows(root, header) { + const headers = getHeaders(root); + const explicit = header.getAttribute("data-column-index"); + const columnIndex = + explicit !== null ? parseInt(explicit, 10) : headers.indexOf(header); + if (columnIndex < 0 || Number.isNaN(columnIndex)) return; + + const current = header.getAttribute("aria-sort"); + const next = current === "ascending" ? "descending" : "ascending"; + + headers.forEach((h) => h.setAttribute("aria-sort", "none")); + header.setAttribute("aria-sort", next); + + const type = header.getAttribute("data-sort-type") || "string"; + const rowsContainer = getRowsContainer(root); + const rows = getRows(rowsContainer); + + rows.sort((rowA, rowB) => { + const cmp = compareValues( + getCellText(rowA, columnIndex), + getCellText(rowB, columnIndex), + type, + ); + return next === "ascending" ? cmp : -cmp; + }); + + rows.forEach((row) => rowsContainer.appendChild(row)); +} + +function attachSortHandlers(root) { + if (root.dataset.sortableInitialised) return; + root.dataset.sortableInitialised = "true"; + + getHeaders(root).forEach((header) => { + if (!header.querySelector("button")) { + const original = header.innerHTML; + header.innerHTML = ``; + } + const button = header.querySelector("button"); + button.addEventListener("click", () => sortRows(root, header)); + }); +} + +function initialiseSortableTables() { + document.querySelectorAll("[data-sortable]").forEach(attachSortHandlers); +} + +function setupMutationObserver() { + let debounceTimer; + const observer = new MutationObserver(() => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(initialiseSortableTables, 50); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + initialiseSortableTables(); + setupMutationObserver(); + }); +} else { + initialiseSortableTables(); + setupMutationObserver(); +} diff --git a/src/js/main.js b/src/js/main.js index 3ebd999..2399d8d 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -2,3 +2,4 @@ import "../scss/components/select/multi-select.ts"; import "./components/data-card/data-card.js"; import "./components/header/header.js"; import "./components/jump-menu/jump-menu.js"; +import "./components/table/sortable-table.js"; diff --git a/src/scss/components/_index.scss b/src/scss/components/_index.scss index 6f5fbdc..9a3b472 100644 --- a/src/scss/components/_index.scss +++ b/src/scss/components/_index.scss @@ -5,6 +5,7 @@ @forward "card/card"; @forward "country-switcher/country-switcher"; @forward "data-card/data-card"; +@forward "file-card/file-card"; @forward "figures/figures"; @forward "form/form"; @forward "piped-list/piped-list"; diff --git a/src/scss/components/file-card/_file-card.scss b/src/scss/components/file-card/_file-card.scss new file mode 100644 index 0000000..c5658c7 --- /dev/null +++ b/src/scss/components/file-card/_file-card.scss @@ -0,0 +1,169 @@ +@use "../../tokens/color" as *; +@use "../../tokens/font" as *; +@use "../../tokens/spacing" as *; + +.iati-file-card-table { + overflow-x: auto; + min-width: 100%; + background: $color-blue-10; + + // Table header that appears visually like a table head + &__header { + display: grid; + grid-template-columns: repeat(5, 1fr); + min-width: 800px; + gap: 0; + border: 1px solid $color-teal-60; + border-bottom: 1px solid $color-teal-60; + background-color: $color-teal-30; + + .iati-file-card-table__header-cell { + padding: 0.5rem 1rem; + text-transform: uppercase; + color: $color-grey-90; + font-weight: 800; + font-size: 0.75rem; + border-right: 1px solid $color-teal-60; + text-align: center; + min-width: 20ch; + + &:last-child { + border-right: none; + } + } + } + + // Container for all file cards + &__cards { + display: flex; + flex-direction: column; + gap: $padding-block; + margin-top: $padding-block; + min-width: 800px; + } +} + +.iati-file-card { + border: 1px solid $color-teal-60; + border-top: 3px solid $color-teal-60; + background: white; + font-size: 0.75rem; + color: $color-teal-90; + padding: 0; + display: flex; + flex-direction: column; + gap: 0; + min-width: 800px; + width: 100%; + text-align: center; + + // First row with 5 cells + &__main-row { + display: grid; + grid-template-columns: repeat(5, 1fr); + min-width: 800px; + gap: 0; + align-items: center; + font-weight: 800; + + .iati-file-card__cell { + padding: 0.5rem 0; + border-right: 1px solid $color-teal-60; + + &:first-child { + text-transform: uppercase; + } + + &:last-child { + border-right: none; + } + } + } + + // Second row with 5 cells + &__details-row { + display: grid; + grid-template-columns: repeat(5, 1fr); + min-width: 800px; + gap: 0; + align-items: stretch; + padding-top: 0; + border-top: 1px solid $color-teal-60; + + .iati-file-card__cell { + padding: 0.5rem 1rem; + border-right: 1px solid $color-teal-60; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 100%; + + .iati-button { + max-width: 125px; + width: auto; + } + + .iati-file-card__chart-container { + .iati-data-card__sparkline { + // width: 100px !important; + height: 30px !important; + max-width: 100%; + } + + .iati-file-card_chart-caption { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + line-height: 14px; + } + } + + &:last-child { + border-right: none; + } + } + } + + // Third row: labeled info cells + &__footer-row { + min-width: 800px; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0; + border-top: 1px solid $color-teal-60; + text-align: center; + + .iati-file-card__cell { + padding: 0.75rem 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + } + } + + &__info-label { + text-transform: uppercase; + font-weight: 800; + font-size: 0.75rem; + color: $color-teal-90; + } +} + +.iati-file-card__status { + font-weight: 700; + + &--success { + color: $color-green-70; + } + + &--error { + color: $color-orange-70; + } + + &--critical { + color: $color-purple-70; + } +} diff --git a/src/scss/components/file-card/file-card.stories.ts b/src/scss/components/file-card/file-card.stories.ts new file mode 100644 index 0000000..7c9a01d --- /dev/null +++ b/src/scss/components/file-card/file-card.stories.ts @@ -0,0 +1,292 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +const meta: Meta = { + title: "Components/File Card", + parameters: { + docs: { + description: { + component: + "A card-based layout for displaying file information that maintains the visual structure of a table header while presenting data in an accessible card format.", + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => html` +
+
+
Package
+
Activities
+
Organisations
+
File size
+
Version
+
+ +
+
+
+
+ Africalia-Activities +
+
11
+
0
+
223.1 kb
+
2.03
+
+
+
+ Package details +
+
+ Activity count +
+
+ Org count +
+
+ Total size +
+
+ IATI version +
+
+ +
+ +
+
+
+ Alcis-Am +
+
1
+
0
+
14.89 kb
+
+
+
+
+ Package details +
+
+ Activity count +
+
+ Org count +
+
+ Total size +
+
+ IATI version +
+
+ +
+ +
+
+
+ Africalia-Org +
+
1
+
0
+
14.89 kb
+
2.03
+
+
+
+ Package details +
+
+ Activity count +
+
+ Org count +
+
+ Total size +
+
+ IATI version +
+
+ +
+
+
+ `, +}; + +export const SingleCard: Story = { + render: () => html` +
+
+
Package
+
Activities
+
Organisations
+
File size
+
Version
+
+ +
+
+
+
+ Example-Package +
+
25
+
3
+
542.7 kb
+
2.03
+
+
+
+ Package details +
+
+ Activity count +
+
+ Org count +
+
+ Total size +
+
+ IATI version +
+
+ +
+
+
+ `, +}; diff --git a/src/scss/components/icon/_icon.scss b/src/scss/components/icon/_icon.scss index 3749efe..f37dfc3 100644 --- a/src/scss/components/icon/_icon.scss +++ b/src/scss/components/icon/_icon.scss @@ -27,6 +27,10 @@ background-image: url("@assets/svg/icon-chevron-left.svg"); } + &--download { + background-image: url("@assets/svg/icon-download.svg"); + } + &--youtube { background-image: url("@assets/svg/youtube-logo.svg"); aspect-ratio: 1.2 / 1; diff --git a/src/scss/components/section/_section.scss b/src/scss/components/section/_section.scss index 0c1d403..a144888 100644 --- a/src/scss/components/section/_section.scss +++ b/src/scss/components/section/_section.scss @@ -26,4 +26,10 @@ background-color: $color-teal-10; } } + &--fill-darker { + .iati-section__content { + padding-inline: 1rem; + background-color: $color-teal-20; + } + } } diff --git a/src/scss/components/section/section.stories.ts b/src/scss/components/section/section.stories.ts index 72f126f..d008a48 100644 --- a/src/scss/components/section/section.stories.ts +++ b/src/scss/components/section/section.stories.ts @@ -16,8 +16,14 @@ export default meta; type Story = StoryObj; const Template = { - render: ({ title, content, fill, headingId }) => html` -
+ render: ({ title, content, fill, fillDarker, headingId }) => html` +

${title}

@@ -75,3 +81,20 @@ export const Fill: Story = { `, }, }; + +export const FillDarker: Story = { + ...Template, + args: { + title: "Section", + fillDarker: true, + content: html` +

+ This section has a darker background using color-teal-20. Lorem ipsum + dolor sit amet, consectetur adipiscing elit. Pellentesque non augue + diam. Morbi nibh arcu, pulvinar sit amet erat ut, gravida imperdiet + erat. +

+ ${Table.render?.call({})} + `, + }, +}; diff --git a/src/scss/components/table/_table.scss b/src/scss/components/table/_table.scss index 8659033..b966044 100644 --- a/src/scss/components/table/_table.scss +++ b/src/scss/components/table/_table.scss @@ -23,6 +23,11 @@ td { background-color: #fff; } + &--plain { + th { + background-color: transparent; + } + } td, th { border-right: 1px solid $color-teal-60; @@ -48,3 +53,39 @@ text-transform: uppercase; } } + +.iati-table, +.iati-file-card-table__header { + [aria-sort] { + cursor: pointer; + } + + [aria-sort] button { + background: none; + border: 0; + padding: 0; + color: inherit; + font: inherit; + text-align: inherit; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.25em; + } + + [aria-sort] button::before { + content: "▴▾"; + opacity: 0.4; + font-size: 0.8em; + } + + [aria-sort="ascending"] button::before { + content: "▴"; + opacity: 1; + } + + [aria-sort="descending"] button::before { + content: "▾"; + opacity: 1; + } +} diff --git a/src/scss/components/table/table.stories.ts b/src/scss/components/table/table.stories.ts index 5e25e17..939e1e7 100644 --- a/src/scss/components/table/table.stories.ts +++ b/src/scss/components/table/table.stories.ts @@ -97,3 +97,41 @@ export const Scrolling: Story = {
`, }; + +export const Sortable: Story = { + render: () => html` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameFilesSize (kB)
Africalia11223.1
Alcis114.89
BRAC27891.4
CAFOD352.6
+
+ `, +}; diff --git a/src/scss/layout/_index.scss b/src/scss/layout/_index.scss index 38f1754..4c7e06d 100644 --- a/src/scss/layout/_index.scss +++ b/src/scss/layout/_index.scss @@ -1,3 +1,4 @@ @forward "page/page"; @forward "masonry/masonry"; @forward "landing-page/landing-page"; +@forward "publishers-page/publishers-page"; diff --git a/src/scss/layout/publishers-page/_publishers-page.scss b/src/scss/layout/publishers-page/_publishers-page.scss new file mode 100644 index 0000000..2237170 --- /dev/null +++ b/src/scss/layout/publishers-page/_publishers-page.scss @@ -0,0 +1,208 @@ +@use "../../base/mixins"; +@use "../../tokens/screens" as *; +@use "../../tokens/color" as *; + +.iati-publishers-page { + @include mixins.page-width-container(); + background-color: white; + flex: 1; + padding-block: 1rem; +} + +.iati-publishers-page__before-content { + margin-block-end: 1rem; + @media (min-width: $screen-md) { + margin-block-end: 2rem; + } + & > * + * { + margin-block-start: 1rem; + } +} + +.iati-publishers-page__intro-row { + display: flex; + flex-direction: column; + gap: 1rem; + border-bottom: 1px solid black; + padding-bottom: 1rem; + @media (min-width: $screen-lg) { + position: relative; + flex-direction: row; + align-items: flex-end; + gap: 2rem; + } +} + +.iati-publishers-page__intro-text { + margin: 0; + color: $color-teal-90; + line-height: 25px; + @media (min-width: $screen-lg) { + width: 75%; + padding-right: 2rem; + } +} + +.iati-publishers-page__intro-button { + @media (min-width: $screen-lg) { + position: absolute; + bottom: 1rem; + right: 0; + } + + .iati-button { + white-space: nowrap; + } +} + +.iati-publishers-page__content { + display: flex; + flex-direction: column-reverse; + gap: 1rem; + @media (min-width: $screen-md) { + flex-direction: row; + gap: 2rem; + } +} + +.iati-publishers-page__article { + flex: 1; + min-width: 0; + & > * + * { + margin-top: 1rem; + @media (min-width: $screen-md) { + margin-top: 2rem; + } + } + > :first-child { + margin-top: 0; + } +} + +.iati-publishers-page__side { + min-inline-size: 15rem; + flex-grow: 0; +} + +.iati-publishers-page__side--sticky > * { + position: sticky; + top: 1rem; + margin-top: 20px; +} + +.iati-publishers-page__after-content { + margin-block-start: 1rem; + @media (min-width: $screen-md) { + margin-block-start: 2rem; + } + & > * + * { + margin-block-start: 1rem; + @media (min-width: $screen-md) { + margin-top: 2rem; + } + } +} + +.iati-publishers-page { + .iati-section__subheading { + font-size: 1.0625rem; + font-weight: 700; + text-transform: uppercase; + } + + .iati-section__header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + + .iati-section__subheading { + flex: 1; + } + } + + .iati-table { + table, + tbody, + thead, + tfoot { + border-color: $color-grey-20; + } + td, + th { + border-right-color: $color-grey-20; + } + tr { + border-bottom-color: $color-grey-20; + } + th { + background-color: $color-teal-30; + } + &--plain th { + background-color: transparent; + } + } + + .iati-subsection_intro { + display: flex; + flex-direction: column; + gap: 1rem; + + @media (min-width: $screen-lg) { + position: relative; + flex-direction: row; + align-items: flex-end; + gap: 2rem; + } + + p { + margin: 0; + + @media (min-width: $screen-lg) { + width: 75%; + } + } + + .iati-button { + align-self: flex-start; + + @media (min-width: $screen-lg) { + position: absolute; + bottom: 0; + right: 0; + } + } + } + + .iati-publishers-page__data-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-top: 1rem; + + .iati-data-card { + max-width: 350px; + } + + .iati-data-card--body { + justify-content: space-between; + flex: 1; + + .iati-data-card__graph { + width: unset; + margin: 0; + } + + .iati-button { + font-size: 14px; + } + } + } + .iati-button { + gap: 0.5rem; + + .iati-icon--download { + height: 1.5rem; + } + } +} diff --git a/src/scss/layout/publishers-page/publishers-page.stories.ts b/src/scss/layout/publishers-page/publishers-page.stories.ts new file mode 100644 index 0000000..73f7009 --- /dev/null +++ b/src/scss/layout/publishers-page/publishers-page.stories.ts @@ -0,0 +1,1118 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { TwoLevel as Breadcrumb } from "../../components/breadcrumb/breadcrumb.stories"; +import { Footer } from "../../components/footer/footer.stories"; +import { Header } from "../../components/header/header.stories"; +import { Default as JumpMenu } from "../../components/jump-menu/jump-menu.stories"; + +const meta: Meta = { + title: "Layout/Publishers Page", + parameters: { + layout: "fullscreen", + backgrounds: { + default: "grey", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const PublishersPage: Story = { + render: (args, context) => html` + ${Header.render?.call({ ...args })} +
+
+ ${Breadcrumb.render( + { + currentPageTitle: "Publisher Page", + previousPageTitles: ["Home", "Reporting Orgs"], + }, + context, + )} +
+
+
+
+

Reporting Org: Publisher Page

+
+

+ A text area next to their name for info and notes about their + own data, publishers provide information about their + organisation and the development or humanitarian activities they + are involved in. This includes details about the organisation + itself, as well as specific information about each activity, + like financial data, contextual information, and results. +

+ +
+
+
+
+

Headlines

+
+
+
+
+
+ On the registry +
+
+ Publisher +
+
+ Reporting org on registry +
+
+ BE-BCE_KBO-0474198059 +
+
+ Reporting org(s) in data +
+
+ BE-BCE_KBO-0474198059 +
+
+
+
+ Hierarchies +
+
+ 1 2 +
+
+ Licenses +
+ +
+ Files failing scheme validation +
+
0
+
+
+
+
+
Activity files
+
2
+
+
+
+ Organisation files +
+
1
+
+
+
Total file size
+
29.8kb
+
+
+
Activities
+
19
+
+
+
+ Unique activities +
+
19
+
+
+
Organisations
+
1
+
+
+
Versions
+
2.03
+
+
+
+
+
+
+
+

Activity Status

+
+
+
+ +
+
+ Description below chart showing changes over time +
+
+ +
+
+
+
+
+

Funding Sources

+
+
+
+ +
+
+ Funding breakdown by source +
+
+ +
+
+
+
+
+

Sectors

+
+
+
+ +
+
Most common sectors
+
+ +
+
+
+
+
+

Countries

+
+
+
+ +
+
Activities by country
+
+ +
+
+
+
+
+

Budget Timeline

+
+
+
+ +
+
Budget trends by year
+
+ +
+
+
+
+
+

Transactions

+
+
+
+ +
+
Transaction patterns
+
+ +
+
+
+
+
+

Results Data

+
+
+
+ +
+
+ Results reporting by quarter +
+
+ +
+
+
+
+
+

+ Exploring Data +

+
+

Files

+
+

+ This is a list of all the files the Reporting Organisation has + uploaded. Up to 50 words here explaining that the package name + might not accurately describe the data. You can access all + file information via an API. +

+ +
+
+
+
+ Package +
+
+ Activities +
+
+ Organisations +
+
+ File size +
+
+ Version +
+
+ +
+
+
+
+ Africalia-Activities +
+
11
+
0
+
223.1 kb
+
+
+
+
+ +
+
Activity count
+
Org count
+
Total size
+
IATI version
+
+ +
+ +
+
+
Alcis-Am
+
1
+
0
+
14.89 kb
+
+
+
+
+ + +
+
+
+ +

+ A short chart 
description here +

+
+
+
+
+ +

+ A short chart 
description here +

+
+
+
+
+ +

+ chart description might go over more than 1 or 
2 + lines +

+
+
+
+
+ +

+ chart 
description +

+
+
+
+ +
+ +
+
+
Africalia-Org
+
1
+
0
+
14.89 kb
+
2.03
+
+
+
+ +
+
Activity count
+
Org count
+
Total size
+
IATI version
+
+ +
+
+
+
+

+ Codelist Values (version 2.xx) +

+ + Download (.JSON) + +
+ +
+

+ Elements and Attributes Published +

+ + Download (.JSON) + +
+ +

+ Organisation Identifiers +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Org Type + Total + Self Refs + Excluding Self Refs
+ Org Elements + + Refs + + Non-Empty Refs + + Org Elements + + Refs + + Non-Empty Refs + + Valid Refs + + Provided +
Accountable1919191900000
Extending000000000
Funding272727819191919100
Implementing37121211261114
Provider1811811811225959595492
Receiver1594040291301111108
+
+
+
+
+

Errors

+

No errors were found

+
+
+

Financial

+
+

Budgets

+

+ The below figures are calculated based on the data contained + within the <budget> element for each reported activity. + Original and revised elements are based on the value declared in + the budget/@type attribute. Where budgets fall across two + calendar years, the month of the <period-end> date is used + to determine annual groupings, with budgets for periods ending + January–June added to the previous calendar year. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Year + Count (all) + + Sum (all) + + Count (Original) + + Sum (Original) + + Count (Revised) + + Sum (Revised) +
202141,250,0003900,0001350,000
202261,820,50051,500,5001320,000
202372,140,75051,640,7502500,000
202451,675,00041,275,0001400,000
+
+
+
+
+ +
+
+ ${Footer.render?.call({ ...args })} + `, +};