diff --git a/astro.config.mjs b/astro.config.mjs index 42c40982..e4e9333a 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -20,18 +20,49 @@ export default defineConfig({ // Same-page and relative links are fine; only flag links whose // target page doesn't exist. errorOnRelativeLinks: false, + // Not all pages are translated; English routes intentionally fall + // back to the Dutch content, so links to them are valid. + errorOnFallbackPages: false, }), ], title: { nl: "RoQua Documentatie", en: "RoQua Documentation", }, + // The top bar is always dark, so the white wordmark works in both themes. logo: { - src: "./src/assets/roqua-q.png", - alt: "RoQua Logo", + src: "./src/assets/wordmark-white.svg", + alt: "RoQua", + replacesTitle: true, }, favicon: "/img/favicon.ico", + components: { + Header: "./src/components/Header.astro", + Hero: "./src/components/Hero.astro", + PageTitle: "./src/components/PageTitle.astro", + }, + + // Code blocks are dark in both themes, per the design; the exact navy + // differs per scheme, so the values live in CSS variables in custom.css. + expressiveCode: { + themes: ["github-dark"], + styleOverrides: { + borderRadius: "0.5rem", + borderColor: "var(--sl-color-hairline)", + codeBackground: "var(--rq-code-bg)", + frames: { + editorTabBarBackground: "var(--rq-code-tabbar-bg)", + editorActiveTabBackground: "var(--rq-code-bg)", + editorActiveTabIndicatorTopColor: "transparent", + editorActiveTabIndicatorBottomColor: "var(--rq-topbar-active)", + terminalBackground: "var(--rq-code-bg)", + terminalTitlebarBackground: "var(--rq-code-tabbar-bg)", + terminalTitlebarBorderBottomColor: "rgb(255 255 255 / 0.08)", + }, + }, + }, + defaultLocale: "root", locales: { root: { label: "Nederlands", lang: "nl" }, @@ -42,7 +73,11 @@ export default defineConfig({ baseUrl: "https://github.com/roqua/documentation/edit/master/", }, - customCss: ["./src/styles/custom.css"], + customCss: [ + "@fontsource-variable/roboto/standard.css", + "@fontsource-variable/roboto/standard-italic.css", + "./src/styles/custom.css", + ], // Recovers the Docusaurus category labels for autogenerated groups. routeMiddleware: "./src/starlightRouteData.ts", diff --git a/package-lock.json b/package-lock.json index 1b1a8f39..45ccc37f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@astrojs/starlight": "^0.41.4", + "@fontsource-variable/roboto": "^5.3.0", "astro": "^7.1.3", "sharp": "^0.34.5" }, @@ -1281,6 +1282,15 @@ "@expressive-code/core": "^0.44.1" } }, + "node_modules/@fontsource-variable/roboto": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/roboto/-/roboto-5.3.0.tgz", + "integrity": "sha512-BoaaZiQf8fgtxv07KXkTwx1bKz/EoqIlTXq2OOKKXINxp6qXWdnlChqV+hSaDANS49yyUASFl3OVBMMDTCIHFA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", diff --git a/package.json b/package.json index 699b218c..fa4ff8bd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@astrojs/starlight": "^0.41.4", + "@fontsource-variable/roboto": "^5.3.0", "astro": "^7.1.3", "sharp": "^0.34.5" }, diff --git a/src/assets/wordmark-white.svg b/src/assets/wordmark-white.svg new file mode 100644 index 00000000..ef3774e2 --- /dev/null +++ b/src/assets/wordmark-white.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/src/assets/wordmark.svg b/src/assets/wordmark.svg new file mode 100644 index 00000000..e33d6f67 --- /dev/null +++ b/src/assets/wordmark.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 00000000..28db4ed1 --- /dev/null +++ b/src/components/Header.astro @@ -0,0 +1,113 @@ +--- +import LanguageSelect from "@astrojs/starlight/components/LanguageSelect.astro"; +import Search from "@astrojs/starlight/components/Search.astro"; +import SiteTitle from "@astrojs/starlight/components/SiteTitle.astro"; +import ThemeSelect from "@astrojs/starlight/components/ThemeSelect.astro"; + +/** + * The dark top bar from the "Ronde 1" design: wordmark, the four documentation + * areas, search, and the theme/language pickers. The active area gets a blue + * underline across the full bar height. + */ + +const { locale } = Astro.locals.starlightRoute; +const base = locale ? `/${locale}` : ""; +const isEnglish = locale === "en"; + +const sections = [ + { label: isEnglish ? "Manual" : "Handleiding", href: `${base}/docs/`, prefixes: [`${base}/docs/`] }, + { label: isEnglish ? "Admin" : "Beheer", href: `${base}/docs/admin/`, prefixes: [`${base}/docs/admin/`] }, + { label: "API", href: `${base}/technical/`, prefixes: [`${base}/technical/`] }, + { + label: isEnglish ? "Data export" : "Data-export", + href: `${base}/technical/csv_export/`, + prefixes: [`${base}/technical/csv_export/`, `${base}/technical/sqlite_export/`], + }, +]; + +// The most specific matching prefix wins, so /technical/csv_export/ highlights +// "Data-export" rather than "API". +const path = Astro.url.pathname.endsWith("/") ? Astro.url.pathname : `${Astro.url.pathname}/`; +const active = sections.reduce( + (best, section) => { + const match = section.prefixes.find((prefix) => path.startsWith(prefix)); + return match && match.length > best.length ? { length: match.length, section } : best; + }, + { length: 0, section: undefined as (typeof sections)[number] | undefined }, +).section; + +--- + +
+
+ +
+ +
+ +
+
+ + +
+
+ + diff --git a/src/components/Hero.astro b/src/components/Hero.astro new file mode 100644 index 00000000..02352778 --- /dev/null +++ b/src/components/Hero.astro @@ -0,0 +1,159 @@ +--- +/** + * The blue search hero from the "Ronde 1" design, shown on pages with `hero` + * frontmatter (the two landing pages). The search box and the quick chips are + * the primary way in; the box opens Starlight's search dialog. + */ + +const { entry, locale } = Astro.locals.starlightRoute; +const { title = entry.data.title, tagline } = entry.data.hero ?? {}; + +const base = locale ? `/${locale}` : ""; +const isEnglish = locale === "en"; + +const placeholder = isEnglish + ? "Search for a page, field name or endpoint" + : "Zoek een pagina, veldnaam of endpoint"; + +const chips = [ + { + label: isEnglish ? "preparing questionnaires" : "vragenlijst klaarzetten", + href: `${base}/docs/epd/measure/measurements/klaarzetten/`, + }, + { label: isEnglish ? "API token" : "API-token", href: `${base}/docs/admin/integration/api_tokens/` }, + { label: isEnglish ? "non-response" : "non-respons", href: `${base}/docs/epd/measure/measurements/non-respons/` }, + { label: isEnglish ? "CSV field names" : "CSV-veldnamen", href: `${base}/technical/csv_export/` }, +]; +--- + +
+
+

+ {tagline &&

} + +

+ {chips.map((chip) => {chip.label})} +
+

+
+ + + + diff --git a/src/components/PageTitle.astro b/src/components/PageTitle.astro new file mode 100644 index 00000000..1e0fad4c --- /dev/null +++ b/src/components/PageTitle.astro @@ -0,0 +1,64 @@ +--- +/** + * Adds the "not yet fully verified" badge from the "Ronde 1" design under the + * title of pages whose frontmatter carries `status: draft`. + */ + +const { entry, locale } = Astro.locals.starlightRoute; +const isDraft = entry.data.status === "draft"; +const label = locale === "en" ? "Not yet fully verified" : "Nog niet volledig geverifieerd"; +--- + +{/* "_top" is Starlight's PAGE_TITLE_ID, which its package doesn't export. */} +

{entry.data.title}

+{ + isDraft && ( +

+ + {label} +

+ ) +} + + diff --git a/src/content/docs/en/index.mdx b/src/content/docs/en/index.mdx index 67106697..58fa8c6b 100644 --- a/src/content/docs/en/index.mdx +++ b/src/content/docs/en/index.mdx @@ -6,15 +6,41 @@ head: - tag: title content: RoQua Documentation hero: - tagline: >- - This site contains the user documentation for RoQua, as well as the technical, - integration-partner documentation. - actions: - - text: User Manual - link: /en/docs/ - icon: right-arrow - - text: Technical documentation - link: /en/technical/ - icon: right-arrow - variant: minimal + title: What can we help you with? + tagline: User manual, admin documentation and technical integration documentation for RoQua. --- + +
+
+
+ +
+

I work with clients

+

Preparing questionnaires, viewing outcomes, reading the timeline.

+ Go to the manual +
+
+
+ +
+

I administer RoQua

+

Configuring protocols and measurements, managing users and roles.

+ Go to admin +
+
+
+ +
+

I'm building an integration

+

REST API, HL7, FHIR and the CSV and SQLite export formats.

+ Go to the API docs +
+
+ +
+
+

New in RoQua

+

Respondents can now start certain questionnaires themselves, as often as they like — without anything having to be prepared for them.

+
+ Read about self-initiation +
diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index 1ab267c5..f0c39fca 100644 --- a/src/content/docs/index.mdx +++ b/src/content/docs/index.mdx @@ -8,19 +8,41 @@ head: - tag: title content: RoQua Documentatie hero: - tagline: >- - Op deze site vindt u de gebruikershandleiding voor RoQua. De handleiding is - opgesplitst in twee delen: de EPD-interface waarop u als medewerker inlogt vanuit - uw EPD-software, en de Admin-interface waar coördinatoren, managers en - applicatiebeheer de configuratie van RoQua kunnen beheren. Naast deze handleiding - voor eindgebruikers hebben we ook API-documentatie voor ontwikkelaars waarin staat - beschreven hoe u uw software met RoQua kunt integreren. - actions: - - text: Gebruikershandleiding - link: /docs/ - icon: right-arrow - - text: Technische handleiding - link: /technical/ - icon: right-arrow - variant: minimal + title: Waar kunnen we je mee helpen? + tagline: Handleiding, beheerdocumentatie en technische koppelingsdocumentatie voor RoQua. --- + +
+
+
+ +
+

Ik werk met cliënten

+

Vragenlijsten klaarzetten, uitkomsten bekijken, tijdlijn lezen.

+ Naar de handleiding +
+
+
+ +
+

Ik beheer RoQua

+

Protocollen en metingen inrichten, gebruikers en rollen beheren.

+ Naar beheer +
+
+
+ +
+

Ik bouw een koppeling

+

REST API, HL7, FHIR en de export-formaten CSV en SQLite.

+ Naar de API-docs +
+
+ +
+
+

Nieuw in RoQua

+

Respondenten kunnen bepaalde vragenlijsten nu zelf opstarten, zo vaak als ze willen — zonder dat er iets klaargezet hoeft te worden.

+
+ Lees over eigen initiatief +
diff --git a/src/styles/custom.css b/src/styles/custom.css index 1d8b5770..9b2ad8eb 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -1,16 +1,379 @@ /* - * Site accent colour, carried over from the Docusaurus theme, which set - * --ifm-color-primary to oklch(0.454 0.135 254) in light mode and - * oklch(0.68 0.16 254) in dark mode. Starlight's `:root` holds the dark theme. + * "Ronde 1" theme: blue accent carried over from the Docusaurus site, a dark + * top bar on every page, and the grey/blue token set from the design. + */ + +/* + * Starlight's `:root` holds the dark theme. The dark palette is "Ronde 2" of + * the design: a blue-grey base on the same blue hue, primary blue lifted to + * oklch(0.74 0.13 254) for contrast on dark, and the hero gradient deepened + * rather than lightened. */ :root { - --sl-color-accent-low: oklch(0.3 0.07 254); - --sl-color-accent: oklch(0.68 0.16 254); - --sl-color-accent-high: oklch(0.86 0.07 254); + --sl-font: "Roboto Variable", system-ui, sans-serif; + + --sl-color-accent-low: oklch(0.36 0.12 256); + --sl-color-accent: oklch(0.46 0.14 254); + --sl-color-accent-high: oklch(0.74 0.13 254); + + --sl-color-white: oklch(0.94 0.008 250); + --sl-color-gray-1: oklch(0.88 0.012 251); + --sl-color-gray-2: oklch(0.8 0.015 253); + --sl-color-gray-3: oklch(0.7 0.018 255); + --sl-color-gray-4: oklch(0.5 0.024 259); + --sl-color-gray-5: oklch(0.34 0.028 262); + --sl-color-gray-6: oklch(0.195 0.024 262); + --sl-color-gray-7: oklch(0.155 0.022 262); + --sl-color-black: oklch(0.245 0.026 262); + + /* Design tokens that differ per scheme but not along Starlight's gray axis. */ + --rq-border: oklch(0.34 0.028 262); + --rq-topbar-bg: oklch(0.155 0.022 262); + /* Accent for underlines on dark surfaces (top bar, code frame tabs). */ + --rq-topbar-active: oklch(0.46 0.14 254); + --rq-hero-gradient: linear-gradient(180deg, oklch(0.46 0.14 254), oklch(0.36 0.12 256)); + --rq-code-bg: oklch(0.155 0.022 262); + --rq-code-tabbar-bg: oklch(0.205 0.024 262); + + /* One border colour everywhere, per the design. */ + --sl-color-hairline-light: var(--rq-border); + --sl-color-hairline: var(--rq-border); + /* Inline code chips use the recessed surface in both schemes. */ + --sl-color-bg-inline-code: var(--sl-color-gray-6); } :root[data-theme="light"] { --sl-color-accent-low: oklch(0.92 0.04 254); --sl-color-accent: oklch(0.454 0.135 254); --sl-color-accent-high: oklch(0.32 0.1 254); + + --sl-color-white: oklch(0.21 0.04 266); + --sl-color-gray-1: oklch(0.28 0.035 266); + --sl-color-gray-2: oklch(0.35 0.03 266); + --sl-color-gray-3: hsl(215.4 16.3% 46.9%); + --sl-color-gray-4: hsl(215.4 16.3% 66%); + --sl-color-gray-5: oklch(0.9 0.013 255.5); + --sl-color-gray-6: oklch(0.95 0.0034 247.86); + --sl-color-gray-7: oklch(0.97 0.003 247.86); + --sl-color-black: #ffffff; + + --rq-border: oklch(0.9 0.013 255.5); + --rq-topbar-bg: oklch(0.21 0.04 266); + --rq-topbar-active: oklch(0.68 0.16 254); + --rq-hero-gradient: linear-gradient(180deg, oklch(0.68 0.16 254), oklch(0.6 0.17 254)); + --rq-code-bg: oklch(0.21 0.04 266); + --rq-code-tabbar-bg: oklch(0.26 0.035 266); +} + +/* ---------------------------------------------------------------- top bar */ + +/* + * The top bar is dark in both color schemes. Remapping the palette variables + * inside the header makes Starlight's own search button and pickers adopt the + * right on-dark colors without overriding their components. + */ +header.header { + --sl-color-bg-nav: var(--rq-topbar-bg); + --sl-color-white: #ffffff; + --sl-color-gray-1: rgb(255 255 255 / 0.85); + --sl-color-gray-2: rgb(255 255 255 / 0.72); + --sl-color-gray-3: rgb(255 255 255 / 0.6); + --sl-color-gray-4: rgb(255 255 255 / 0.45); + --sl-color-gray-5: rgb(255 255 255 / 0.25); + --sl-color-gray-6: rgb(255 255 255 / 0.1); + --sl-color-gray-7: rgb(255 255 255 / 0.06); + --sl-color-black: var(--rq-topbar-bg); + --sl-color-text-accent: #ffffff; + --sl-color-hairline-shade: transparent; + + border-bottom: 1px solid transparent; +} + +header.header site-search button { + background: rgb(255 255 255 / 0.1); + border-color: rgb(255 255 255 / 0.16); + color: rgb(255 255 255 / 0.6); +} +header.header site-search button:hover { + border-color: rgb(255 255 255 / 0.4); + color: #fff; +} +header.header site-search kbd { + background: transparent; + border: 1px solid rgb(255 255 255 / 0.25); +} + +/* ---------------------------------------------------------------- sidebar */ + +:root { + --sl-color-bg-sidebar: var(--sl-color-bg); +} + +@media (min-width: 50rem) { + .sidebar-pane { + border-inline-end: 1px solid var(--sl-color-hairline); + } +} + +/* Group labels as small uppercase headings. */ +#starlight__sidebar .group-label > span { + font-size: var(--sl-text-xs); + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--sl-color-gray-3); +} + +/* The active page: no pill, just a blue edge on the list's guide line. */ +#starlight__sidebar a[aria-current="page"], +#starlight__sidebar a[aria-current="page"]:hover, +#starlight__sidebar a[aria-current="page"]:focus { + background: transparent; + color: var(--sl-color-text-accent); + font-weight: 500; + /* A straight edge: without this the shadow follows the link's rounded corners. */ + border-radius: 0; + box-shadow: inset 3px 0 0 0 var(--sl-color-text-accent); +} +[dir="rtl"] #starlight__sidebar a[aria-current="page"], +[dir="rtl"] #starlight__sidebar a[aria-current="page"]:hover, +[dir="rtl"] #starlight__sidebar a[aria-current="page"]:focus { + box-shadow: inset -3px 0 0 0 var(--sl-color-text-accent); +} + +/* ---------------------------------------------------- table of contents */ + +.right-sidebar { + border-inline-start: 1px solid var(--sl-color-hairline); +} + +.right-sidebar starlight-toc h2 { + font-size: var(--sl-text-xs); + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--sl-color-gray-3); +} + +.right-sidebar starlight-toc a { + border-inline-start: 2px solid var(--sl-color-hairline); + border-radius: 0; + color: var(--sl-color-gray-3); + --pad-inline: 0.75rem; +} +.right-sidebar starlight-toc a:hover { + color: var(--sl-color-white); +} +.right-sidebar starlight-toc a[aria-current="true"] { + border-inline-start-color: var(--sl-color-text-accent); + color: var(--sl-color-text-accent); + font-weight: 500; + background: transparent; +} + +/* ----------------------------------------------------------------- prose */ + +.sl-markdown-content a:not(:where(.not-content *)) { + text-underline-offset: 3px; +} +.sl-markdown-content a:not(:where(.not-content *)):hover { + text-decoration-style: dotted; +} + +.sl-markdown-content :is(h1, h2, h3, h4):not(:where(.not-content *)) { + font-weight: 600; + letter-spacing: -0.005em; +} + +/* Callouts: bordered card with a colored edge, per the design. */ +.starlight-aside { + border: 1px solid var(--sl-color-hairline); + border-inline-start: 3px solid var(--sl-color-asides-border); + border-radius: 0.375rem; + padding: 1rem 1.125rem; +} +.starlight-aside--note { + --sl-color-asides-border: var(--sl-color-text-accent); + --sl-color-asides-text-accent: var(--sl-color-text-accent); + background: var(--sl-color-gray-6); + color: var(--sl-color-gray-1); +} +.starlight-aside--caution { + --sl-color-asides-border: oklch(0.82 0.15 95); + --sl-color-asides-text-accent: oklch(0.82 0.15 95); + background: oklch(0.82 0.15 95 / 0.12); + color: var(--sl-color-gray-1); +} +:root[data-theme="light"] .starlight-aside--caution { + --sl-color-asides-border: oklch(0.6 0.225 95); + --sl-color-asides-text-accent: oklch(0.6 0.225 95); + background: oklch(0.9 0.165 98 / 0.14); +} + +/* Tables: full-width bordered cards with a grey header row. */ +.sl-markdown-content table:not(:where(.not-content *)) { + display: table; + width: 100%; + border: 1px solid var(--sl-color-hairline); + border-radius: 0.5rem; + border-collapse: separate; + border-spacing: 0; + overflow: hidden; + font-size: var(--sl-text-sm); + line-height: 1.5; +} +.sl-markdown-content table:not(:where(.not-content *)) th { + background: var(--sl-color-gray-6); + font-size: var(--sl-text-xs); + font-weight: 600; + color: var(--sl-color-gray-3); + text-align: start; + padding: 0.625rem 1rem; +} +.sl-markdown-content table:not(:where(.not-content *)) td { + padding: 0.75rem 1rem; + border-top: 1px solid var(--sl-color-hairline); +} +.sl-markdown-content :is(th, td):not(:where(.not-content *)) { + border-inline: 0; + border-bottom: 0; +} +.sl-markdown-content tr:nth-child(2n):not(:where(.not-content *)) { + background: transparent; +} + +/* Prev/next: cards with a small grey label, no arrows. */ +.pagination-links a { + border-radius: 0.5rem; + padding: 0.875rem 1rem; + box-shadow: none; + gap: 0.25rem; +} +.pagination-links a svg { + display: none; +} +.pagination-links a > span { + font-size: var(--sl-text-xs); + color: var(--sl-color-gray-3); +} +.pagination-links .link-title { + font-size: 0.9375rem; + font-weight: 500; + color: var(--sl-color-text-accent); +} + +/* ---------------------------------------------------------- landing page */ + +/* Let the hero band bleed to the viewport edges without a horizontal scrollbar. */ +[data-has-hero] .main-pane { + overflow-x: clip; +} +[data-has-hero] .main-frame .content-panel + .content-panel { + border-top: 0; +} +[data-has-hero] main { + background: var(--sl-color-gray-6); +} + +.rq-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-top: 1.25rem; +} +@media (max-width: 50rem) { + .rq-cards { + grid-template-columns: 1fr; + } +} + +.rq-card { + background: var(--sl-color-bg); + border: 1px solid var(--sl-color-hairline); + border-radius: 0.5rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + padding: 1.375rem; + display: flex; + flex-direction: column; + gap: 0.625rem; +} +.rq-card-icon { + width: 2.25rem; + height: 2.25rem; + border-radius: 0.375rem; + background: var(--sl-color-gray-6); + display: flex; + align-items: center; + justify-content: center; + color: var(--sl-color-text-accent); +} +.rq-card h2 { + margin: 0; + font-size: 1.0625rem; + font-weight: 600; + color: var(--sl-color-white); +} +.rq-card p { + margin: 0; + font-size: var(--sl-text-sm); + line-height: 1.5; + color: var(--sl-color-gray-3); + text-wrap: pretty; +} +.rq-card > a { + margin-top: auto; + padding-top: 0.375rem; + font-size: var(--sl-text-sm); + font-weight: 500; + color: var(--sl-color-text-accent); + text-decoration: none; +} +.rq-card > a:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.rq-banner { + margin-top: 1.5rem; + background: var(--sl-color-bg); + border: 1px solid var(--sl-color-hairline); + border-radius: 0.5rem; + padding: 1.5rem; + display: flex; + gap: 2.5rem; + align-items: center; + flex-wrap: wrap; +} +.rq-banner-text { + flex: 1; + min-width: 16rem; + display: flex; + flex-direction: column; + gap: 0.375rem; +} +.rq-banner h2 { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: var(--sl-color-white); +} +.rq-banner p { + margin: 0; + font-size: var(--sl-text-sm); + line-height: 1.5; + color: var(--sl-color-gray-3); + text-wrap: pretty; +} +.rq-banner > a { + display: inline-flex; + align-items: center; + height: 2.5rem; + padding: 0 1.125rem; + border-radius: 0.375rem; + background: var(--sl-color-text-accent); + color: #fff; + font-size: 0.9375rem; + font-weight: 500; + text-decoration: none; + flex: none; }