diff --git a/css/site-theme.css b/css/site-theme.css index 1abbadc8a..48b59c9c2 100644 --- a/css/site-theme.css +++ b/css/site-theme.css @@ -209,3 +209,815 @@ padding-inline: var(--utrecht-button-padding-inline-start, 1.25em) var(--utrecht-button-padding-inline-end, 1.25em); } + +/* + * ============================================================================ + * THE CHROME THE SCHOOL DESIGNS DRAW (site-chrome-follows-the-design). + * + * Everything below reads tokens only. The theme app's public bridge hands the + * site these, from whatever set the portal uses (thematiq + * brand-motif-on-portals): + * + * --cn-brand-stripe-* the motif: three bands or an image, and its + * inverse for a dark band. Unset for a set + * without a stripe: then nothing is drawn. + * --thematiq-accent-* the accent (current menu item, badges) + * --thematiq-badge-* a count badge + * --thematiq-logo-text-font-size 0 when the logo carries the name + * --thematiq-footer-legal-background-color the footer's bottom band + * --thematiq-hero-* a light hero, when the set draws one + * + * and site.php names the set's light logo (--nldesign-logo-inverse-url) and + * its emblem (--nldesign-emblem-url) as absolute addresses. + * ============================================================================ + */ + +/* + * THE MOTIF UNDER THE HEADER AND OVER THE FOOTER (G-01). + * + * Drawn as a pseudo-element, so the header and footer markup of every portal + * stays as it was and the motif costs the site's entry nothing. The height is + * the set's stripe height, and 0 for a set that declares no stripe: such a + * portal draws nothing here. + * + * Under the header the motif is the image when the set names one, else one + * line in the stripe's first colour: Zuiddrecht's header carries a solid red + * line (canvas v13), its three bands sit on the footer. + */ +.pq-site .pq-site__header .ac-header__navigation-secondary, +.pq-site .pq-site__header .ac-header__navigation-main:only-child, +.pq-site .ac-footer .pq-footer__band--content { + position: relative; +} + +.pq-site .pq-site__header .ac-header__navigation-secondary::after, +.pq-site + .pq-site__header:not(:has(.ac-header__navigation-secondary)) + .ac-header__navigation-main::after { + content: ''; + position: absolute; + inset-inline: 0; + inset-block-start: 100%; + z-index: 1; + block-size: var(--cn-brand-stripe-height, 0); + pointer-events: none; + background-image: var( + --cn-brand-stripe-image, + linear-gradient( + var(--cn-brand-stripe-color-1), + var(--cn-brand-stripe-color-1) + ) + ); + background-repeat: repeat-x; + background-size: auto 100%; +} + +/* The motif hangs below the menu bar; the breadcrumb starts under it. */ +.pq-site .pq-site__header .ac-header__navigation-breadcrumb { + padding-block-start: var(--cn-brand-stripe-height, 0); +} + +.pq-site .ac-footer .pq-footer__band--content::before { + --pq-stripe-first: var(--cn-brand-stripe-ratio-1, 1); + --pq-stripe-second: var(--cn-brand-stripe-ratio-2, 1); + --pq-stripe-total: calc( + var(--pq-stripe-first) + var(--pq-stripe-second) + + var(--cn-brand-stripe-ratio-3, 1) + ); + --pq-stripe-stop-1: calc(var(--pq-stripe-first) / var(--pq-stripe-total) * 100%); + --pq-stripe-stop-2: calc( + (var(--pq-stripe-first) + var(--pq-stripe-second)) / var(--pq-stripe-total) * + 100% + ); + content: ''; + position: absolute; + inset-inline: 0; + inset-block-start: 0; + block-size: var(--cn-brand-stripe-height, 0); + pointer-events: none; + background-image: var( + --cn-brand-stripe-image-inverse, + linear-gradient( + to right, + var(--cn-brand-stripe-color-1) 0 var(--pq-stripe-stop-1), + var(--cn-brand-stripe-color-2) var(--pq-stripe-stop-1) + var(--pq-stripe-stop-2), + var(--cn-brand-stripe-color-3) var(--pq-stripe-stop-2) 100% + ) + ); + background-repeat: repeat-x; + background-size: auto 100%; +} + +/* + * THE LOGO. A wordmark set (the logo carries the name) keeps the portal's + * title for screen readers only; the logo sits at the start of its box, so a + * wide wordmark lines up with the content column under it. + */ +.pq-site .ac-header__logo .logo-text { + font-size: var( + --thematiq-logo-text-font-size, + var(--tilburg-typography-font-size-3xl) + ); +} + +.pq-site .ac-footer__logo > .con-logo-container + span, +.pq-site .ac-footer__logo > .con-logo-container + span > span { + font-size: var(--thematiq-logo-text-font-size, inherit); +} + +/* With a wordmark the name block folds to nothing (0 times its size); a + * set without the token keeps it, 100 times its size being no limit. */ +.pq-site .ac-footer__logo > .con-logo-container + span { + max-block-size: calc(var(--thematiq-logo-text-font-size, 100) * 1em); + overflow: hidden; +} + +.pq-site .con-logo-container.header, +.pq-site .con-logo-container.footer { + background-position: 0 50%; +} + +/* + * THE DESIGNED HEADER (G-02, G-03, G-07): logo, search, one way to the own + * area on a white bar, the menu on a bar under it, both on the content + * column. Only a portal that declares `headerSearch` or `accountLabel`. + */ +.pq-site .pq-site__header--designed .ac-header__navigation-main { + --navigation-bar-height: 96px; + border-block-end: 1px solid + var(--nldesign-color-border, var(--tilburg-line-border-color)); + padding-inline: max(16px, calc((100% - 1232px) / 2)); +} + +.pq-site .pq-site__header--designed .ac-header__right-section { + margin-inline-end: 0; +} + +.pq-site .pq-site__header--designed .ac-header__navigation-secondary .container { + max-inline-size: 1232px; + margin-inline: auto; + padding-inline: 16px; +} + +.pq-site .pq-site__header--designed .ac-c-navigation__link-container { + position: relative; + padding-block: 14px; + padding-inline: 14px; +} + +.pq-site .pq-site__header--designed .ac-c-navigation__label { + color: var(--nldesign-color-text, var(--tilburg-document-color)); + font-size: 1.0625rem; + font-weight: 500; +} + +/* The page on screen: bold, with a bar in the accent under it (in the + * primary for a set without an accent). */ +.pq-site .ac-header .ac-c-navigation__link-container[aria-current='page'] { + background-color: transparent; +} + +.pq-site + .ac-header + .ac-c-navigation__link-container[aria-current='page'] + .ac-c-navigation__label { + font-weight: 700; +} + +.pq-site .ac-header .ac-c-navigation__link-container[aria-current='page']::after { + content: ''; + position: absolute; + inset-inline: 0; + inset-block-end: 0; + block-size: 4px; + background-color: var(--thematiq-accent-color, var(--nldesign-color-primary)); +} + +.pq-site .ac-header .ac-c-navigation__li { + position: relative; +} + +.pq-header-tools { + display: flex; + align-items: center; + gap: 12px; +} + +.pq-header-tools__search { + position: relative; + display: flex; +} + +.pq-header-tools__input.utrecht-textbox { + inline-size: 240px; + block-size: 46px; + padding-inline: 16px 46px; + border: 1px solid + var(--nldesign-color-border-dark, var(--utrecht-textbox-border-color)); + border-radius: var( + --nldesign-website-border-radius, + var(--utrecht-textbox-border-radius, 4px) + ); + background-color: var( + --nldesign-color-background, + var(--utrecht-textbox-background-color) + ); + color: var(--nldesign-color-text, var(--utrecht-textbox-color)); + font: inherit; +} + +.pq-header-tools__submit { + position: absolute; + inset-block: 0; + inset-inline-end: 0; + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: 46px; + padding: 0; + border: 0; + background: transparent; + color: var(--nldesign-color-primary, currentcolor); +} + +.pq-header-tools svg { + inline-size: 20px; + block-size: 20px; + flex: none; +} + +.pq-header-tools__account.utrecht-button-link { + display: inline-flex; + align-items: center; + gap: 8px; + min-block-size: 46px; + padding-inline: 20px; + border-radius: var( + --nldesign-website-border-radius, + var(--utrecht-button-border-radius, 4px) + ); + text-decoration: none; +} + +.pq-header-tools__chip { + display: inline-flex; + align-items: center; + gap: 10px; + min-block-size: 46px; + padding-block: 4px; + padding-inline: 4px 16px; + border: 1px solid var(--nldesign-color-border, var(--tilburg-line-border-color)); + border-radius: var(--nldesign-website-border-radius-large, 999px); + color: var(--nldesign-color-text, var(--tilburg-document-color)); + text-decoration: none; +} + +.pq-header-tools__chip:hover { + background-color: var( + --nldesign-color-background-hover, + var(--tilburg-color-gray-100) + ); +} + +.pq-header-tools__initials { + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: 36px; + block-size: 36px; + border-radius: var(--nldesign-website-border-radius, 50%); + background-color: var( + --nldesign-color-primary-light, + var(--tilburg-color-blue-100) + ); + color: var(--nldesign-color-primary-hover, var(--nldesign-color-primary)); + font-size: 0.8125rem; + font-weight: 700; +} + +.pq-header-tools__who { + display: flex; + flex-direction: column; + line-height: 1.25; +} + +.pq-header-tools__name { + font-weight: 700; +} + +.pq-header-tools__subline { + color: var(--nldesign-color-text-muted, var(--tilburg-document-subtle-color)); + font-size: 0.8125rem; +} + +.pq-header-tools__signout { + padding: 0; + border: 0; + background: transparent; + color: var(--nldesign-color-link, var(--utrecht-link-color)); + font: inherit; + font-weight: 700; + text-decoration: underline; + text-underline-offset: 3px; +} + +.pq-header-tools__menu { + display: none; + align-items: center; + gap: 8px; + min-block-size: 44px; + padding-inline: 12px; + border: 1px solid var(--nldesign-color-border-dark, currentcolor); + border-radius: var(--nldesign-website-border-radius, 4px); + background: transparent; + color: var(--nldesign-color-text, currentcolor); + font: inherit; + font-weight: 600; +} + +/* + * ON A PHONE (G-07): logo and one menu button. The button opens the menu bar + * and the search box; the person chip shrinks to its initials. + */ +@media (max-width: 767px) { + .pq-site .pq-site__header--designed .ac-header__navigation-main { + --navigation-bar-height: 72px; + flex-wrap: wrap; + row-gap: 8px; + padding-block: 12px; + } + + .pq-site .pq-site__header--designed .ac-header__right-section { + margin-inline: 0; + } + + .pq-header-tools { + flex-wrap: wrap; + justify-content: flex-end; + } + + .pq-header-tools__menu { + display: inline-flex; + } + + .pq-header-tools__search { + display: none; + order: 3; + inline-size: 100%; + } + + .pq-header-tools__search--open { + display: flex; + } + + .pq-header-tools__search .pq-header-tools__input.utrecht-textbox { + inline-size: 100%; + } + + .pq-header-tools__who { + position: absolute; + inline-size: 1px; + block-size: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + } + + .pq-header-tools__chip { + padding-inline-end: 4px; + } + + .pq-site .pq-site__header--designed .ac-header__navigation-secondary { + display: none; + } + + /* The menu bar folds away, so the motif runs under the masthead. */ + .pq-site .pq-site__header--designed .ac-header__navigation-main { + position: relative; + } + + .pq-site + .pq-site__header--designed:not(:has(.pq-site__nav--open)) + .ac-header__navigation-main::after { + content: ''; + position: absolute; + inset-inline: 0; + inset-block-start: 100%; + z-index: 1; + block-size: var(--cn-brand-stripe-height, 0); + pointer-events: none; + background-image: var( + --cn-brand-stripe-image, + linear-gradient( + var(--cn-brand-stripe-color-1), + var(--cn-brand-stripe-color-1) + ) + ); + background-repeat: repeat-x; + background-size: auto 100%; + } + + .pq-site + .pq-site__header--designed + .ac-header__navigation-secondary.pq-site__nav--open { + display: block; + } + + .pq-site .pq-site__header--designed .pq-site__nav--open .ac-c-navigation__ul { + flex-direction: column; + align-items: stretch; + } +} + +/* The new chrome speaks the set's own face, not the vendored Roboto. */ +.pq-site .pq-header-tools, +.pq-site .pq-signin, +.pq-site .pq-site__footer { + font-family: var(--nldesign-body-font-family, inherit); +} + +/* + * THE DESIGNED FOOTER (G-04): brand column first with its button, the + * contact column, the menus; the bottom band one step darker than the footer. + */ +.ac-footer section.pq-footer__band--designed.pq-footer__band .container { + grid-template-columns: minmax(0, 2fr) repeat(3, minmax(0, 1fr)); + column-gap: 48px; +} + +@media screen and (max-width: 1024px) { + .ac-footer section.pq-footer__band--designed.pq-footer__band .container { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media screen and (max-width: 768px) { + .ac-footer section.pq-footer__band--designed.pq-footer__band .container { + grid-template-columns: minmax(0, 1fr); + } +} + +.pq-site .pq-footer__band--designed .ac-footer__logo { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0; +} + +.ac-footer section.pq-footer__band--designed.pq-footer__band ul { + padding-inline-start: 0; + list-style: none; +} + +.ac-footer section.pq-footer__band--designed.pq-footer__band ul li { + padding-inline-start: 0; +} + +.pq-site .pq-footer__band--designed .ac-footer__menu-title { + margin-block-end: var(--tilburg-space-block-mouse, 16px); + font-size: 1.125rem; +} + +.pq-site .ac-footer .ac-footer__logo .con-logo-container.footer { + margin-block-end: var(--tilburg-space-block-rabbit, 24px); +} + +.pq-site .ac-footer .pq-footer__description { + max-inline-size: 26em; + margin-block-end: var(--tilburg-space-block-rabbit, 24px); + line-height: 1.6; +} + +.pq-site .ac-footer .pq-footer__cta.utrecht-button-link { + display: inline-flex; + align-items: center; + min-block-size: 48px; + padding-inline: 20px; + border: 2px solid currentcolor; + border-radius: var( + --nldesign-website-border-radius, + var(--utrecht-button-border-radius, 4px) + ); + background-color: transparent; + color: var(--tilburg-footer-color, inherit); + font-weight: 700; + text-decoration: none; +} + +.pq-site .ac-footer .pq-footer__cta.utrecht-button-link:hover { + background-color: var(--tilburg-footer-color, currentcolor); + color: var(--tilburg-footer-background-color, inherit); +} + +.pq-site .ac-footer .pq-footer__contact p { + margin-block-end: var(--tilburg-space-block-snail, 12px); +} + +.pq-site .ac-footer .pq-footer__contact a, +.pq-site .ac-footer .ac-footer__link { + text-decoration: underline; + text-underline-offset: 3px; +} + +.pq-site .ac-footer section.pq-footer__band--legal.pq-footer__band { + background-color: var( + --thematiq-footer-legal-background-color, + var(--tilburg-color-blue-500) + ); +} + +/* + * THE HERO (G-08): a light ground and dark type when the set draws its hero + * that way, and the set's emblem large and faint at the end of the band. + */ +.pq-site .ac-hero { + position: relative; + overflow: hidden; + background-color: var( + --thematiq-hero-background-color, + var(--tilburg-color-blue-500) + ); +} + +.pq-site .ac-hero::after { + content: ''; + position: absolute; + inset-block: 8%; + inset-inline-end: max(16px, calc((100% - 1232px) / 2)); + inline-size: min(36%, 420px); + pointer-events: none; + background-image: var(--thematiq-hero-decoration-image, none); + background-position: 100% 50%; + background-repeat: no-repeat; + background-size: contain; + opacity: 0.16; +} + +.pq-site .ac-hero > * { + position: relative; + z-index: 1; +} + +.pq-site .ac-hero .ac-hero__title, +.pq-site .ac-hero .ac-hero__subtitle { + color: var(--nldesign-hero-title-color, var(--utrecht-document-inverse-color)); +} + +.pq-site .ac-hero .ac-hero__subtitle { + color: var(--nldesign-hero-body-color, var(--utrecht-document-inverse-color)); +} + +.pq-site .ac-hero .pq-hero__action { + border-radius: var(--nldesign-website-border-radius, 0); +} + +.pq-site .ac-hero .pq-hero__action:first-child { + border-color: var(--thematiq-hero-action-background-color, currentcolor); + background-color: var(--thematiq-hero-action-background-color, transparent); + color: var( + --thematiq-hero-action-color, + var(--nldesign-hero-title-color, var(--utrecht-document-inverse-color)) + ); +} + +/* + * THE SIGN-IN PAGE AS ROLE CARDS (G-18). + */ +.pq-signin { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 56px; + padding-block: 40px 64px; +} + +.pq-signin--with-panel { + grid-template-columns: minmax(0, 1fr) minmax(0, 520px); + align-items: start; +} + +.pq-signin__title.utrecht-heading-1 { + margin-block-end: 16px; + overflow-wrap: anywhere; +} + +.pq-signin__main { + min-inline-size: 0; +} + +.pq-signin__intro.utrecht-paragraph { + max-inline-size: 46em; + margin-block-end: 32px; + font-size: 1.25rem; + line-height: 1.6; +} + +.pq-signin__cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); + gap: 24px; + max-inline-size: 1000px; + margin: 0 0 32px; + padding: 0; + list-style: none; +} + +.pq-signin__card { + display: flex; + flex-direction: column; + gap: 16px; + padding: 28px; + border: 1px solid var(--nldesign-color-border, var(--tilburg-line-border-color)); + border-radius: var(--nldesign-website-border-radius-large, 8px); + background-color: var(--nldesign-color-background, var(--tilburg-color-white)); +} + +.pq-signin__card-head { + display: flex; + align-items: flex-start; + gap: 16px; +} + +.pq-signin__card-title.utrecht-heading-3 { + margin: 0; + line-height: 1.25; +} + +.pq-signin__card-text.utrecht-paragraph { + margin-block-start: 6px; +} + +.pq-signin__mark { + display: inline-flex; + flex: none; + align-items: center; + justify-content: center; + inline-size: 56px; + block-size: 56px; + border-radius: var(--nldesign-website-border-radius, 8px); + background-color: var( + --nldesign-color-primary-light, + var(--tilburg-color-blue-100) + ); + color: var(--nldesign-color-primary, currentcolor); + font-size: 0.9375rem; + font-weight: 700; +} + +.pq-signin__mark svg { + inline-size: 28px; + block-size: 28px; +} + +.pq-signin__mark--digid, +.pq-signin__mark--eherkenning, +.pq-signin__mark--eidas { + background-color: var(--nldesign-color-text, CanvasText); + color: var(--nldesign-color-background, Canvas); +} + +.pq-signin__button.utrecht-button-link { + display: flex; + align-self: stretch; + inline-size: 100%; + max-inline-size: none; + align-items: center; + justify-content: center; + min-block-size: 52px; + border-radius: var( + --nldesign-website-border-radius, + var(--utrecht-button-border-radius, 4px) + ); + font-size: 1.0625rem; + font-weight: 700; + text-decoration: none; +} + +.pq-signin__button.utrecht-button-link--secondary-action { + border: 2px solid var(--nldesign-color-primary, currentcolor); +} + +.pq-signin__hint { + margin: 0; + color: var(--nldesign-color-text-muted, var(--tilburg-document-subtle-color)); + font-size: 0.9375rem; +} + +.pq-signin__notice { + display: flex; + gap: 12px; + max-inline-size: 1000px; + margin-block-end: 32px; + padding: 20px 24px; + border: 1px solid + var(--nldesign-color-primary-light-hover, var(--nldesign-color-border)); + border-radius: var(--nldesign-website-border-radius, 4px); + background-color: var( + --nldesign-color-primary-light, + var(--tilburg-color-blue-100) + ); + line-height: 1.6; +} + +.pq-signin__notice svg { + flex: none; + inline-size: 22px; + block-size: 22px; + margin-block-start: 2px; +} + +.pq-signin__notice-title { + font-weight: 700; +} + +.pq-signin__panel { + position: relative; + overflow: hidden; + padding: 48px 36px 72px; + border-radius: var(--nldesign-website-border-radius-large, 8px); + background-color: var( + --nldesign-color-footer-background, + var(--nldesign-color-primary-hover) + ); + color: var(--nldesign-color-footer-text, var(--nldesign-color-primary-text)); +} + +/* The motif along the panel's top, its emblem large and faint at the foot. */ +.pq-signin__panel::before { + content: ''; + position: absolute; + inset-inline: 0; + inset-block-start: 0; + block-size: var(--cn-brand-stripe-height, 0); + background-image: var(--cn-brand-stripe-image-inverse, none); + background-repeat: repeat-x; + background-size: auto 100%; +} + +.pq-signin__panel::after { + content: ''; + position: absolute; + inset-inline-end: -20px; + inset-block-end: -20px; + inline-size: 240px; + block-size: 240px; + pointer-events: none; + background-image: var(--nldesign-emblem-url, none); + background-repeat: no-repeat; + background-size: contain; + opacity: 0.12; +} + +.pq-signin__panel-title.utrecht-heading-3 { + margin-block-end: 24px; + color: inherit; +} + +.pq-signin__points { + position: relative; + z-index: 1; + display: grid; + gap: 20px; + margin: 0; + padding: 0; + list-style: none; +} + +.pq-signin__point { + display: flex; + gap: 16px; +} + +.pq-signin__point-mark { + display: inline-flex; + flex: none; + align-items: center; + justify-content: center; + inline-size: 36px; + block-size: 36px; + border-radius: 50%; + background-color: var(--thematiq-accent-light-color, transparent); + color: var(--thematiq-accent-text-color, currentcolor); +} + +.pq-signin__point-mark svg { + inline-size: 18px; + block-size: 18px; +} + +.pq-signin__point-title { + display: block; + font-weight: 700; + font-size: 1.125rem; +} + +.pq-signin__point-text { + display: block; + opacity: 0.9; +} + +@media (max-width: 1023px) { + .pq-signin--with-panel { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/l10n/en.js b/l10n/en.js index 71030a8b6..e5f9ff4ce 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -2094,7 +2094,125 @@ OC.L10N.register( "Where a submission goes. `case` creates an object in caseRegister and caseSchema. `wooRequest` hands it to opencatalogi's Woo intake, which mints the request reference and starts the statutory term; the form's fields are then named after the request's own fields (`requestedInformation`, `requesterName`, `requesterEmail`, `requesterPhone`, `requesterAddress`). Without opencatalogi a `wooRequest` submission is marked failed.": "Where a submission goes. `case` creates an object in caseRegister and caseSchema. `wooRequest` hands it to opencatalogi's Woo intake, which mints the request reference and starts the statutory term; the form's fields are then named after the request's own fields (`requestedInformation`, `requesterName`, `requesterEmail`, `requesterPhone`, `requesterAddress`). Without opencatalogi a `wooRequest` submission is marked failed.", "The reference the receiving app minted, such as a Woo request's `WOO-2026-1A2B3C`. Set only once the submission is registered.": "The reference the receiving app minted, such as a Woo request's `WOO-2026-1A2B3C`. Set only once the submission is registered.", "Due at": "Due at", - "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.": "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started." + "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.": "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.", + "A box under the cards, for example what to do the first time.": "A box under the cards, for example what to do the first time.", + "A column of contact details beside the footer text: an address, a phone number, an e-mail address. A line with a link is shown as a link.": "A column of contact details beside the footer text: an address, a phone number, an e-mail address. A line with a link is shown as a link.", + "A line under the cards for staff, with a link to where they sign in.": "A line under the cards for staff, with a link to where they sign in.", + "A panel beside the cards with what the visitor finds after signing in.": "A panel beside the cards with what the visitor finds after signing in.", + "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. Off unless switched on.": "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. Off unless switched on.", + "A short line under the button, such as what you need to sign in.": "A short line under the button, such as what you need to sign in.", + "Button text": "Button text", + "Button to the own area": "Button to the own area", + "Card heading": "Card heading", + "Card text": "Card text", + "Column heading": "Column heading", + "Contact column": "Contact column", + "Footer button": "Footer button", + "Hint": "Hint", + "Hint in the box": "Hint in the box", + "Introduction": "Introduction", + "Lines": "Lines", + "Link for staff": "Link for staff", + "One button under the footer text, such as a link to the contact page. Left out when it has no label or no destination.": "One button under the footer text, such as a link to the contact page. Left out when it has no label or no destination.", + "One line more about it.": "One line more about it.", + "One or two sentences under the heading: who signs in how.": "One or two sentences under the heading: who signs in how.", + "Points": "Points", + "Search in the header": "Search in the header", + "Search page": "Search page", + "Show the search box": "Show the search box", + "Side panel": "Side panel", + "Sign-in cards": "Sign-in cards", + "Sign-in page": "Sign-in page", + "The heading above the lines, such as Contact.": "The heading above the lines, such as Contact.", + "The heading of the box.": "The heading of the box.", + "The heading of the panel.": "The heading of the panel.", + "The lines of the column, in order.": "The lines of the column, in order.", + "The name of an icon for the card. DigiD and eHerkenning show their own mark.": "The name of an icon for the card. DigiD and eHerkenning show their own mark.", + "The name of an icon for the point.": "The name of an icon for the point.", + "The page heading, such as Inloggen bij Mijn Wilgenboom. Empty shows Welkom.": "The page heading, such as Inloggen bij Mijn Wilgenboom. Empty shows Welkom.", + "The page of this portal that shows the results, such as /zoeken. Empty or not a page of this portal means /zoeken.": "The page of this portal that shows the results, such as /zoeken. Empty or not a page of this portal means /zoeken.", + "The point in a few words.": "The point in a few words.", + "The points in the panel.": "The points in the panel.", + "The sentence before the link, such as Werkt u bij De Wilgenboom?": "The sentence before the link, such as Werkt u bij De Wilgenboom?", + "The short text in the empty box, which is also its name for screen readers. Empty shows \"Zoeken\".": "The short text in the empty box, which is also its name for screen readers. Empty shows \"Zoeken\".", + "The text around the sign-in cards. Every part is optional.": "The text around the sign-in cards. Every part is optional.", + "The text of one button in the header that opens the visitor's own area, where they choose how to sign in. Empty shows a link for each way to sign in instead.": "The text of one button in the header that opens the visitor's own area, where they choose how to sign in. Empty shows a link for each way to sign in instead.", + "The text of the box.": "The text of the box.", + "The text of the card for each way to sign in on the sign-in page, keyed by mode (digid, nextcloud, eherkenning, oidc, local, eidas). A mode without a card shows one plain button.": "The text of the card for each way to sign in on the sign-in page, keyed by mode (digid, nextcloud, eherkenning, oidc, local, eidas). A mode without a card shows one plain button.", + "The text of the link.": "The text of the link.", + "The text of the sign-in button. Empty shows the standard text for this way in.": "The text of the sign-in button. Empty shows the standard text for this way in.", + "What the line says.": "What the line says.", + "What the visitor can do after signing in this way.": "What the visitor can do after signing in this way.", + "Where the line links to: a page of this portal, or a web, mail or phone address. Empty shows plain text.": "Where the line links to: a page of this portal, or a web, mail or phone address. Empty shows plain text.", + "Where the link goes.": "Where the link goes.", + "Where the link goes: a page of this portal such as /contact, or a full web, mail or phone address.": "Where the link goes: a page of this portal such as /contact, or a full web, mail or phone address.", + "Whether the header shows the search box.": "Whether the header shows the search box.", + "Who this way in is for, such as Ik ben leerling.": "Who this way in is for, such as Ik ben leerling.", + "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. A portal that declares it shows it, unless it switches it off.": "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. A portal that declares it shows it, unless it switches it off.", + "False hides a declared search box. Empty shows it.": "False hides a declared search box. Empty shows it.", + "Name for screen readers": "Name for screen readers", + "What a screen reader calls the box, such as Zoeken op de website. Empty uses the hint.": "What a screen reader calls the box, such as Zoeken op de website. Empty uses the hint.", + "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks).": "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks).", + "On a greeting: whether today's date shows; on unless false.": "On a greeting: whether today's date shows; on unless false.", + "On a segmented kpi: a fixed whole.": "On a segmented kpi: a fixed whole.", + "On a segmented kpi: the field with the whole.": "On a segmented kpi: the field with the whole.", + "On a segmented kpi: the unit after the figure.": "On a segmented kpi: the unit after the figure.", + "On a segmented kpi: up to five parts, each field, label and tone.": "On a segmented kpi: up to five parts, each field, label and tone.", + "On bars and chips: the label of each row.": "On bars and chips: the label of each row.", + "On bars: the figure a full bar stands for.": "On bars: the figure a full bar stands for.", + "On bars: the figure of each row.": "On bars: the figure of each row.", + "On bars: the grey head line, from the first row.": "On bars: the grey head line, from the first row.", + "On bars: the small heading above the note.": "On bars: the small heading above the note.", + "On cards and highlight tasks: the fields of the second line.": "On cards and highlight tasks: the fields of the second line.", + "On cards: the coming-up part.": "On cards: the coming-up part.", + "On cards: the first letter of the title in a circle.": "On cards: the first letter of the title in a circle.", + "On cards: the small heading above the coming-up part.": "On cards: the small heading above the coming-up part.", + "On cards: the text beside the status; on bars: the note under the bars, from the first row.": "On cards: the text beside the status; on bars: the note under the bars, from the first row.", + "On chips: a mark below this is marked as below the pass mark.": "On chips: a mark below this is marked as below the pass mark.", + "On chips: the average of each row.": "On chips: the average of each row.", + "On chips: the list of marks of each row.": "On chips: the list of marks of each row.", + "On highlight tasks: the small uppercase label.": "On highlight tasks: the small uppercase label.", + "On highlight tasks: the words on the button.": "On highlight tasks: the words on the button.", + "On rows and cards: the field shown as a status pill.": "On rows and cards: the field shown as a status pill.", + "On rows and cards: the tone of each status value (neutral, success, warning, error).": "On rows and cards: the tone of each status value (neutral, success, warning, error).", + "On rows: the field drawn as a date tile.": "On rows: the field drawn as a date tile.", + "On rows: the line shown in quotes.": "On rows: the line shown in quotes.", + "On rows: the line under the status pill.": "On rows: the line under the status pill.", + "On rows: the second line.": "On rows: the second line.", + "On the website": "On the website", + "The field with the date or moment before which a row is not shown, in a list, an inbox or a read by id, on the server clock (site-school-blocks).": "The field with the date or moment before which a row is not shown, in a list, an inbox or a read by id, on the server clock (site-school-blocks).", + "The slug of the portal whose public website shows this item when `public` is on. Empty means no website shows it.": "The slug of the portal whose public website shows this item when `public` is on. Empty means no website shows it.", + "The words the public website shows beside the date to say who the item is for, such as 'groep 7' or 'hele school'. Written by staff, because the site never reads the school app's groups for a visitor who is not signed in. Empty shows nothing.": "The words the public website shows beside the date to say who the item is for, such as 'groep 7' or 'hele school'. Written by staff, because the site never reads the school app's groups for a visitor who is not signed in. Empty shows nothing.", + "Visible from field": "Visible from field", + "Website": "Website", + "Whether the item also shows on the public website of `portal`, for visitors who are not signed in. Off by default: a news item is for its audience only until staff turn this on. An item that targets single children never shows publicly, whatever this says (PublicNewsReader).": "Whether the item also shows on the public website of `portal`, for visitors who are not signed in. Off by default: a news item is for its audience only until staff turn this on. An item that targets single children never shows publicly, whatever this says (PublicNewsReader).", + "Who it is for, on the website": "Who it is for, on the website", + "avatar": "avatar", + "averageField": "averageField", + "buttonLabel": "buttonLabel", + "captionField": "captionField", + "dateField": "dateField", + "eyebrow": "eyebrow", + "labelField": "labelField", + "lowBelow": "lowBelow", + "max": "max", + "noteField": "noteField", + "noteLabel": "noteLabel", + "quoteField": "quoteField", + "segments": "segments", + "showDate": "showDate", + "soonField": "soonField", + "soonLabel": "soonLabel", + "statusField": "statusField", + "statusNoteField": "statusNoteField", + "statusTones": "statusTones", + "subtitleField": "subtitleField", + "subtitleFields": "subtitleFields", + "target": "target", + "totalField": "totalField", + "unit": "unit", + "valueField": "valueField", + "valuesField": "valuesField" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index e073e1ea0..7306e2460 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -2105,7 +2105,125 @@ "Where a submission goes. `case` creates an object in caseRegister and caseSchema. `wooRequest` hands it to opencatalogi's Woo intake, which mints the request reference and starts the statutory term; the form's fields are then named after the request's own fields (`requestedInformation`, `requesterName`, `requesterEmail`, `requesterPhone`, `requesterAddress`). Without opencatalogi a `wooRequest` submission is marked failed.": "Where a submission goes. `case` creates an object in caseRegister and caseSchema. `wooRequest` hands it to opencatalogi's Woo intake, which mints the request reference and starts the statutory term; the form's fields are then named after the request's own fields (`requestedInformation`, `requesterName`, `requesterEmail`, `requesterPhone`, `requesterAddress`). Without opencatalogi a `wooRequest` submission is marked failed.", "The reference the receiving app minted, such as a Woo request's `WOO-2026-1A2B3C`. Set only once the submission is registered.": "The reference the receiving app minted, such as a Woo request's `WOO-2026-1A2B3C`. Set only once the submission is registered.", "Due at": "Due at", - "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.": "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started." + "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.": "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.", + "A box under the cards, for example what to do the first time.": "A box under the cards, for example what to do the first time.", + "A column of contact details beside the footer text: an address, a phone number, an e-mail address. A line with a link is shown as a link.": "A column of contact details beside the footer text: an address, a phone number, an e-mail address. A line with a link is shown as a link.", + "A line under the cards for staff, with a link to where they sign in.": "A line under the cards for staff, with a link to where they sign in.", + "A panel beside the cards with what the visitor finds after signing in.": "A panel beside the cards with what the visitor finds after signing in.", + "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. Off unless switched on.": "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. Off unless switched on.", + "A short line under the button, such as what you need to sign in.": "A short line under the button, such as what you need to sign in.", + "Button text": "Button text", + "Button to the own area": "Button to the own area", + "Card heading": "Card heading", + "Card text": "Card text", + "Column heading": "Column heading", + "Contact column": "Contact column", + "Footer button": "Footer button", + "Hint": "Hint", + "Hint in the box": "Hint in the box", + "Introduction": "Introduction", + "Lines": "Lines", + "Link for staff": "Link for staff", + "One button under the footer text, such as a link to the contact page. Left out when it has no label or no destination.": "One button under the footer text, such as a link to the contact page. Left out when it has no label or no destination.", + "One line more about it.": "One line more about it.", + "One or two sentences under the heading: who signs in how.": "One or two sentences under the heading: who signs in how.", + "Points": "Points", + "Search in the header": "Search in the header", + "Search page": "Search page", + "Show the search box": "Show the search box", + "Side panel": "Side panel", + "Sign-in cards": "Sign-in cards", + "Sign-in page": "Sign-in page", + "The heading above the lines, such as Contact.": "The heading above the lines, such as Contact.", + "The heading of the box.": "The heading of the box.", + "The heading of the panel.": "The heading of the panel.", + "The lines of the column, in order.": "The lines of the column, in order.", + "The name of an icon for the card. DigiD and eHerkenning show their own mark.": "The name of an icon for the card. DigiD and eHerkenning show their own mark.", + "The name of an icon for the point.": "The name of an icon for the point.", + "The page heading, such as Inloggen bij Mijn Wilgenboom. Empty shows Welkom.": "The page heading, such as Inloggen bij Mijn Wilgenboom. Empty shows Welkom.", + "The page of this portal that shows the results, such as /zoeken. Empty or not a page of this portal means /zoeken.": "The page of this portal that shows the results, such as /zoeken. Empty or not a page of this portal means /zoeken.", + "The point in a few words.": "The point in a few words.", + "The points in the panel.": "The points in the panel.", + "The sentence before the link, such as Werkt u bij De Wilgenboom?": "The sentence before the link, such as Werkt u bij De Wilgenboom?", + "The short text in the empty box, which is also its name for screen readers. Empty shows \"Zoeken\".": "The short text in the empty box, which is also its name for screen readers. Empty shows \"Zoeken\".", + "The text around the sign-in cards. Every part is optional.": "The text around the sign-in cards. Every part is optional.", + "The text of one button in the header that opens the visitor's own area, where they choose how to sign in. Empty shows a link for each way to sign in instead.": "The text of one button in the header that opens the visitor's own area, where they choose how to sign in. Empty shows a link for each way to sign in instead.", + "The text of the box.": "The text of the box.", + "The text of the card for each way to sign in on the sign-in page, keyed by mode (digid, nextcloud, eherkenning, oidc, local, eidas). A mode without a card shows one plain button.": "The text of the card for each way to sign in on the sign-in page, keyed by mode (digid, nextcloud, eherkenning, oidc, local, eidas). A mode without a card shows one plain button.", + "The text of the link.": "The text of the link.", + "The text of the sign-in button. Empty shows the standard text for this way in.": "The text of the sign-in button. Empty shows the standard text for this way in.", + "What the line says.": "What the line says.", + "What the visitor can do after signing in this way.": "What the visitor can do after signing in this way.", + "Where the line links to: a page of this portal, or a web, mail or phone address. Empty shows plain text.": "Where the line links to: a page of this portal, or a web, mail or phone address. Empty shows plain text.", + "Where the link goes.": "Where the link goes.", + "Where the link goes: a page of this portal such as /contact, or a full web, mail or phone address.": "Where the link goes: a page of this portal such as /contact, or a full web, mail or phone address.", + "Whether the header shows the search box.": "Whether the header shows the search box.", + "Who this way in is for, such as Ik ben leerling.": "Who this way in is for, such as Ik ben leerling.", + "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. A portal that declares it shows it, unless it switches it off.": "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. A portal that declares it shows it, unless it switches it off.", + "False hides a declared search box. Empty shows it.": "False hides a declared search box. Empty shows it.", + "Name for screen readers": "Name for screen readers", + "What a screen reader calls the box, such as Zoeken op de website. Empty uses the hint.": "What a screen reader calls the box, such as Zoeken op de website. Empty uses the hint.", + "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks).": "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks).", + "On a greeting: whether today's date shows; on unless false.": "On a greeting: whether today's date shows; on unless false.", + "On a segmented kpi: a fixed whole.": "On a segmented kpi: a fixed whole.", + "On a segmented kpi: the field with the whole.": "On a segmented kpi: the field with the whole.", + "On a segmented kpi: the unit after the figure.": "On a segmented kpi: the unit after the figure.", + "On a segmented kpi: up to five parts, each field, label and tone.": "On a segmented kpi: up to five parts, each field, label and tone.", + "On bars and chips: the label of each row.": "On bars and chips: the label of each row.", + "On bars: the figure a full bar stands for.": "On bars: the figure a full bar stands for.", + "On bars: the figure of each row.": "On bars: the figure of each row.", + "On bars: the grey head line, from the first row.": "On bars: the grey head line, from the first row.", + "On bars: the small heading above the note.": "On bars: the small heading above the note.", + "On cards and highlight tasks: the fields of the second line.": "On cards and highlight tasks: the fields of the second line.", + "On cards: the coming-up part.": "On cards: the coming-up part.", + "On cards: the first letter of the title in a circle.": "On cards: the first letter of the title in a circle.", + "On cards: the small heading above the coming-up part.": "On cards: the small heading above the coming-up part.", + "On cards: the text beside the status; on bars: the note under the bars, from the first row.": "On cards: the text beside the status; on bars: the note under the bars, from the first row.", + "On chips: a mark below this is marked as below the pass mark.": "On chips: a mark below this is marked as below the pass mark.", + "On chips: the average of each row.": "On chips: the average of each row.", + "On chips: the list of marks of each row.": "On chips: the list of marks of each row.", + "On highlight tasks: the small uppercase label.": "On highlight tasks: the small uppercase label.", + "On highlight tasks: the words on the button.": "On highlight tasks: the words on the button.", + "On rows and cards: the field shown as a status pill.": "On rows and cards: the field shown as a status pill.", + "On rows and cards: the tone of each status value (neutral, success, warning, error).": "On rows and cards: the tone of each status value (neutral, success, warning, error).", + "On rows: the field drawn as a date tile.": "On rows: the field drawn as a date tile.", + "On rows: the line shown in quotes.": "On rows: the line shown in quotes.", + "On rows: the line under the status pill.": "On rows: the line under the status pill.", + "On rows: the second line.": "On rows: the second line.", + "On the website": "On the website", + "The field with the date or moment before which a row is not shown, in a list, an inbox or a read by id, on the server clock (site-school-blocks).": "The field with the date or moment before which a row is not shown, in a list, an inbox or a read by id, on the server clock (site-school-blocks).", + "The slug of the portal whose public website shows this item when `public` is on. Empty means no website shows it.": "The slug of the portal whose public website shows this item when `public` is on. Empty means no website shows it.", + "The words the public website shows beside the date to say who the item is for, such as 'groep 7' or 'hele school'. Written by staff, because the site never reads the school app's groups for a visitor who is not signed in. Empty shows nothing.": "The words the public website shows beside the date to say who the item is for, such as 'groep 7' or 'hele school'. Written by staff, because the site never reads the school app's groups for a visitor who is not signed in. Empty shows nothing.", + "Visible from field": "Visible from field", + "Website": "Website", + "Whether the item also shows on the public website of `portal`, for visitors who are not signed in. Off by default: a news item is for its audience only until staff turn this on. An item that targets single children never shows publicly, whatever this says (PublicNewsReader).": "Whether the item also shows on the public website of `portal`, for visitors who are not signed in. Off by default: a news item is for its audience only until staff turn this on. An item that targets single children never shows publicly, whatever this says (PublicNewsReader).", + "Who it is for, on the website": "Who it is for, on the website", + "avatar": "avatar", + "averageField": "averageField", + "buttonLabel": "buttonLabel", + "captionField": "captionField", + "dateField": "dateField", + "eyebrow": "eyebrow", + "labelField": "labelField", + "lowBelow": "lowBelow", + "max": "max", + "noteField": "noteField", + "noteLabel": "noteLabel", + "quoteField": "quoteField", + "segments": "segments", + "showDate": "showDate", + "soonField": "soonField", + "soonLabel": "soonLabel", + "statusField": "statusField", + "statusNoteField": "statusNoteField", + "statusTones": "statusTones", + "subtitleField": "subtitleField", + "subtitleFields": "subtitleFields", + "target": "target", + "totalField": "totalField", + "unit": "unit", + "valueField": "valueField", + "valuesField": "valuesField" }, "plurals": "", "pluralForm": "nplurals=2; plural=(n != 1);" diff --git a/l10n/nl.js b/l10n/nl.js index abf54e280..34707c013 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -2159,7 +2159,125 @@ OC.L10N.register( "Where a submission goes. `case` creates an object in caseRegister and caseSchema. `wooRequest` hands it to opencatalogi's Woo intake, which mints the request reference and starts the statutory term; the form's fields are then named after the request's own fields (`requestedInformation`, `requesterName`, `requesterEmail`, `requesterPhone`, `requesterAddress`). Without opencatalogi a `wooRequest` submission is marked failed.": "Waar een inzending heen gaat. `case` maakt een object in caseRegister en caseSchema. `wooRequest` geeft haar aan de Woo-intake van opencatalogi, die het kenmerk van het verzoek uitgeeft en de wettelijke termijn start; de velden van het formulier heten dan zoals de velden van het verzoek (`requestedInformation`, `requesterName`, `requesterEmail`, `requesterPhone`, `requesterAddress`). Zonder opencatalogi wordt een `wooRequest`-inzending als mislukt gemarkeerd.", "The reference the receiving app minted, such as a Woo request's `WOO-2026-1A2B3C`. Set only once the submission is registered.": "Het kenmerk dat de ontvangende app uitgaf, zoals `WOO-2026-1A2B3C` van een Woo-verzoek. Alleen gezet zodra de inzending is geregistreerd.", "Due at": "Uiterste datum", - "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.": "De wettelijke uiterste datum die de ontvangende app startte, zoals de beslistermijn van een Woo-verzoek. Alleen gezet als er een termijn loopt, zodat de statuspagina nooit een termijn noemt die niemand startte." + "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.": "De wettelijke uiterste datum die de ontvangende app startte, zoals de beslistermijn van een Woo-verzoek. Alleen gezet als er een termijn loopt, zodat de statuspagina nooit een termijn noemt die niemand startte.", + "A box under the cards, for example what to do the first time.": "Een vak onder de kaarten, bijvoorbeeld wat u de eerste keer doet.", + "A column of contact details beside the footer text: an address, a phone number, an e-mail address. A line with a link is shown as a link.": "Een kolom met contactgegevens naast de tekst in de voettekst: een adres, een telefoonnummer, een e-mailadres. Een regel met een link wordt als link getoond.", + "A line under the cards for staff, with a link to where they sign in.": "Een regel onder de kaarten voor medewerkers, met een link naar de plek waar zij inloggen.", + "A panel beside the cards with what the visitor finds after signing in.": "Een vak naast de kaarten met wat de bezoeker vindt na het inloggen.", + "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. Off unless switched on.": "Een zoekvak in de kop, op elke pagina. Het opent de zoekpagina van het portaal met wat de bezoeker typte. Uit tot u het aanzet.", + "A short line under the button, such as what you need to sign in.": "Een korte regel onder de knop, bijvoorbeeld wat u nodig heeft om in te loggen.", + "Button text": "Tekst op de knop", + "Button to the own area": "Knop naar de eigen omgeving", + "Card heading": "Kop van de kaart", + "Card text": "Tekst van de kaart", + "Column heading": "Kop van de kolom", + "Contact column": "Kolom met contactgegevens", + "Footer button": "Knop in de voettekst", + "Hint": "Hint", + "Hint in the box": "Hint in het vak", + "Introduction": "Inleiding", + "Lines": "Regels", + "Link for staff": "Link voor medewerkers", + "One button under the footer text, such as a link to the contact page. Left out when it has no label or no destination.": "Eén knop onder de tekst in de voettekst, bijvoorbeeld naar de contactpagina. Weggelaten als de tekst of de bestemming ontbreekt.", + "One line more about it.": "Eén regel meer erover.", + "One or two sentences under the heading: who signs in how.": "Een of twee zinnen onder de kop: wie logt hoe in.", + "Points": "Punten", + "Search in the header": "Zoeken in de kop", + "Search page": "Zoekpagina", + "Show the search box": "Zoekvak tonen", + "Side panel": "Zijvak", + "Sign-in cards": "Inlogkaarten", + "Sign-in page": "Inlogpagina", + "The heading above the lines, such as Contact.": "De kop boven de regels, bijvoorbeeld Contact.", + "The heading of the box.": "De kop van het vak.", + "The heading of the panel.": "De kop van het vak.", + "The lines of the column, in order.": "De regels van de kolom, op volgorde.", + "The name of an icon for the card. DigiD and eHerkenning show their own mark.": "De naam van een pictogram voor de kaart. DigiD en eHerkenning tonen hun eigen logo.", + "The name of an icon for the point.": "De naam van een pictogram voor het punt.", + "The page heading, such as Inloggen bij Mijn Wilgenboom. Empty shows Welkom.": "De kop van de pagina, bijvoorbeeld Inloggen bij Mijn Wilgenboom. Leeg toont Welkom.", + "The page of this portal that shows the results, such as /zoeken. Empty or not a page of this portal means /zoeken.": "De pagina van dit portaal die de resultaten toont, bijvoorbeeld /zoeken. Leeg of geen pagina van dit portaal betekent /zoeken.", + "The point in a few words.": "Het punt in een paar woorden.", + "The points in the panel.": "De punten in het vak.", + "The sentence before the link, such as Werkt u bij De Wilgenboom?": "De zin voor de link, bijvoorbeeld Werkt u bij De Wilgenboom?", + "The short text in the empty box, which is also its name for screen readers. Empty shows \"Zoeken\".": "De korte tekst in het lege vak, die ook de naam voor schermlezers is. Leeg toont \"Zoeken\".", + "The text around the sign-in cards. Every part is optional.": "De tekst rond de inlogkaarten. Elk deel mag leeg blijven.", + "The text of one button in the header that opens the visitor's own area, where they choose how to sign in. Empty shows a link for each way to sign in instead.": "De tekst van één knop in de kop die de eigen omgeving van de bezoeker opent, waar die kiest hoe hij inlogt. Leeg toont in plaats daarvan een link voor elke manier van inloggen.", + "The text of the box.": "De tekst van het vak.", + "The text of the card for each way to sign in on the sign-in page, keyed by mode (digid, nextcloud, eherkenning, oidc, local, eidas). A mode without a card shows one plain button.": "De tekst van de kaart voor elke manier van inloggen op de inlogpagina, per manier (digid, nextcloud, eherkenning, oidc, local, eidas). Een manier zonder kaart toont één gewone knop.", + "The text of the link.": "De tekst van de link.", + "The text of the sign-in button. Empty shows the standard text for this way in.": "De tekst van de inlogknop. Leeg toont de gewone tekst voor deze manier van inloggen.", + "What the line says.": "Wat de regel zegt.", + "What the visitor can do after signing in this way.": "Wat de bezoeker kan doen na het inloggen op deze manier.", + "Where the line links to: a page of this portal, or a web, mail or phone address. Empty shows plain text.": "Waar de regel naar linkt: een pagina van dit portaal, of een web-, mail- of telefoonadres. Leeg toont gewone tekst.", + "Where the link goes.": "Waar de link naartoe gaat.", + "Where the link goes: a page of this portal such as /contact, or a full web, mail or phone address.": "Waar de link naartoe gaat: een pagina van dit portaal zoals /contact, of een volledig web-, mail- of telefoonadres.", + "Whether the header shows the search box.": "Of de kop het zoekvak toont.", + "Who this way in is for, such as Ik ben leerling.": "Voor wie deze manier van inloggen is, bijvoorbeeld Ik ben leerling.", + "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. A portal that declares it shows it, unless it switches it off.": "Een zoekvak in de kop, op elke pagina. Het opent de zoekpagina van het portaal met wat de bezoeker typte. Een portaal dat het opgeeft toont het, tenzij het uit staat.", + "False hides a declared search box. Empty shows it.": "Uit verbergt een opgegeven zoekvak. Leeg toont het.", + "Name for screen readers": "Naam voor schermlezers", + "What a screen reader calls the box, such as Zoeken op de website. Empty uses the hint.": "Hoe een schermlezer het vak noemt, bijvoorbeeld Zoeken op de website. Leeg gebruikt de hint.", + "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks).": "Op een lijstblok: kaarten, rijen met datum, balken of cijferlabels in plaats van een tabel; bij taken de uitgelichte kaart; bij kerncijfers één verdeelde balk; bij de kalender datumtegels (site-school-blocks).", + "On a greeting: whether today's date shows; on unless false.": "Bij een begroeting: of de datum van vandaag zichtbaar is; aan tenzij uit.", + "On a segmented kpi: a fixed whole.": "Bij een verdeeld kerncijfer: een vast totaal.", + "On a segmented kpi: the field with the whole.": "Bij een verdeeld kerncijfer: het veld met het totaal.", + "On a segmented kpi: the unit after the figure.": "Bij een verdeeld kerncijfer: de eenheid na het getal.", + "On a segmented kpi: up to five parts, each field, label and tone.": "Bij een verdeeld kerncijfer: hoogstens vijf delen, elk met veld, label en toon.", + "On bars and chips: the label of each row.": "Bij balken en cijferlabels: het label van elke rij.", + "On bars: the figure a full bar stands for.": "Bij balken: het getal dat een volle balk betekent.", + "On bars: the figure of each row.": "Bij balken: het getal van elke rij.", + "On bars: the grey head line, from the first row.": "Bij balken: de grijze kopregel, uit de eerste rij.", + "On bars: the small heading above the note.": "Bij balken: de kleine kop boven de notitie.", + "On cards and highlight tasks: the fields of the second line.": "Bij kaarten en uitgelichte taken: de velden van de tweede regel.", + "On cards: the coming-up part.": "Bij kaarten: het deel dat binnenkort komt.", + "On cards: the first letter of the title in a circle.": "Bij kaarten: de eerste letter van de titel in een cirkel.", + "On cards: the small heading above the coming-up part.": "Bij kaarten: de kleine kop boven het deel dat binnenkort komt.", + "On cards: the text beside the status; on bars: the note under the bars, from the first row.": "Bij kaarten: de tekst naast de status; bij balken: de notitie onder de balken, uit de eerste rij.", + "On chips: a mark below this is marked as below the pass mark.": "Bij cijferlabels: een cijfer hieronder krijgt het teken onvoldoende.", + "On chips: the average of each row.": "Bij cijferlabels: het gemiddelde van elke rij.", + "On chips: the list of marks of each row.": "Bij cijferlabels: de lijst met cijfers van elke rij.", + "On highlight tasks: the small uppercase label.": "Bij uitgelichte taken: het kleine label in hoofdletters.", + "On highlight tasks: the words on the button.": "Bij uitgelichte taken: de tekst op de knop.", + "On rows and cards: the field shown as a status pill.": "Bij rijen en kaarten: het veld dat als statuslabel verschijnt.", + "On rows and cards: the tone of each status value (neutral, success, warning, error).": "Bij rijen en kaarten: de toon van elke statuswaarde (neutraal, gelukt, waarschuwing, fout).", + "On rows: the field drawn as a date tile.": "Bij rijen: het veld dat als datumtegel verschijnt.", + "On rows: the line shown in quotes.": "Bij rijen: de regel tussen aanhalingstekens.", + "On rows: the line under the status pill.": "Bij rijen: de regel onder het statuslabel.", + "On rows: the second line.": "Bij rijen: de tweede regel.", + "On the website": "Op de website", + "The field with the date or moment before which a row is not shown, in a list, an inbox or a read by id, on the server clock (site-school-blocks).": "Het veld met de datum of het moment waarvoor een rij niet zichtbaar is, in een lijst, een postvak of een opvraging per id, volgens de klok van de server (site-school-blocks).", + "The slug of the portal whose public website shows this item when `public` is on. Empty means no website shows it.": "De slug van het portaal waarvan de openbare website dit bericht toont als `public` aan staat. Leeg betekent dat geen website het toont.", + "The words the public website shows beside the date to say who the item is for, such as 'groep 7' or 'hele school'. Written by staff, because the site never reads the school app's groups for a visitor who is not signed in. Empty shows nothing.": "De woorden die de openbare website naast de datum toont om te zeggen voor wie het bericht is, zoals 'groep 7' of 'hele school'. Geschreven door medewerkers, omdat de site de groepen van de school niet leest voor een bezoeker die niet is ingelogd. Leeg toont niets.", + "Visible from field": "Veld zichtbaar vanaf", + "Website": "Website", + "Whether the item also shows on the public website of `portal`, for visitors who are not signed in. Off by default: a news item is for its audience only until staff turn this on. An item that targets single children never shows publicly, whatever this says (PublicNewsReader).": "Of het bericht ook op de openbare website van `portal` staat, voor bezoekers die niet zijn ingelogd. Standaard uit: een bericht is alleen voor de eigen doelgroep tot medewerkers dit aanzetten. Een bericht voor losse kinderen staat nooit openbaar, wat hier ook staat (PublicNewsReader).", + "Who it is for, on the website": "Voor wie, op de website", + "avatar": "letter in een cirkel", + "averageField": "veld met het gemiddelde", + "buttonLabel": "tekst op de knop", + "captionField": "veld met de kopregel", + "dateField": "veld met de datum", + "eyebrow": "label boven de titel", + "labelField": "veld met het label", + "lowBelow": "onvoldoende onder", + "max": "maximum", + "noteField": "veld met de notitie", + "noteLabel": "kop boven de notitie", + "quoteField": "veld met het citaat", + "segments": "delen", + "showDate": "datum tonen", + "soonField": "veld met wat binnenkort komt", + "soonLabel": "kop boven wat binnenkort komt", + "statusField": "veld met de status", + "statusNoteField": "veld met de statusnotitie", + "statusTones": "tonen per status", + "subtitleField": "veld met de tweede regel", + "subtitleFields": "velden van de tweede regel", + "target": "doel", + "totalField": "veld met het totaal", + "unit": "eenheid", + "valueField": "veld met de waarde", + "valuesField": "veld met de waarden" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index b5bc6674e..6f6cf7337 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -2170,7 +2170,125 @@ "Where a submission goes. `case` creates an object in caseRegister and caseSchema. `wooRequest` hands it to opencatalogi's Woo intake, which mints the request reference and starts the statutory term; the form's fields are then named after the request's own fields (`requestedInformation`, `requesterName`, `requesterEmail`, `requesterPhone`, `requesterAddress`). Without opencatalogi a `wooRequest` submission is marked failed.": "Waar een inzending heen gaat. `case` maakt een object in caseRegister en caseSchema. `wooRequest` geeft haar aan de Woo-intake van opencatalogi, die het kenmerk van het verzoek uitgeeft en de wettelijke termijn start; de velden van het formulier heten dan zoals de velden van het verzoek (`requestedInformation`, `requesterName`, `requesterEmail`, `requesterPhone`, `requesterAddress`). Zonder opencatalogi wordt een `wooRequest`-inzending als mislukt gemarkeerd.", "The reference the receiving app minted, such as a Woo request's `WOO-2026-1A2B3C`. Set only once the submission is registered.": "Het kenmerk dat de ontvangende app uitgaf, zoals `WOO-2026-1A2B3C` van een Woo-verzoek. Alleen gezet zodra de inzending is geregistreerd.", "Due at": "Uiterste datum", - "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.": "De wettelijke uiterste datum die de ontvangende app startte, zoals de beslistermijn van een Woo-verzoek. Alleen gezet als er een termijn loopt, zodat de statuspagina nooit een termijn noemt die niemand startte." + "The statutory due date the receiving app armed, such as a Woo request's decision term. Set only when a term runs, so the reference page never quotes a deadline nobody started.": "De wettelijke uiterste datum die de ontvangende app startte, zoals de beslistermijn van een Woo-verzoek. Alleen gezet als er een termijn loopt, zodat de statuspagina nooit een termijn noemt die niemand startte.", + "A box under the cards, for example what to do the first time.": "Een vak onder de kaarten, bijvoorbeeld wat u de eerste keer doet.", + "A column of contact details beside the footer text: an address, a phone number, an e-mail address. A line with a link is shown as a link.": "Een kolom met contactgegevens naast de tekst in de voettekst: een adres, een telefoonnummer, een e-mailadres. Een regel met een link wordt als link getoond.", + "A line under the cards for staff, with a link to where they sign in.": "Een regel onder de kaarten voor medewerkers, met een link naar de plek waar zij inloggen.", + "A panel beside the cards with what the visitor finds after signing in.": "Een vak naast de kaarten met wat de bezoeker vindt na het inloggen.", + "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. Off unless switched on.": "Een zoekvak in de kop, op elke pagina. Het opent de zoekpagina van het portaal met wat de bezoeker typte. Uit tot u het aanzet.", + "A short line under the button, such as what you need to sign in.": "Een korte regel onder de knop, bijvoorbeeld wat u nodig heeft om in te loggen.", + "Button text": "Tekst op de knop", + "Button to the own area": "Knop naar de eigen omgeving", + "Card heading": "Kop van de kaart", + "Card text": "Tekst van de kaart", + "Column heading": "Kop van de kolom", + "Contact column": "Kolom met contactgegevens", + "Footer button": "Knop in de voettekst", + "Hint": "Hint", + "Hint in the box": "Hint in het vak", + "Introduction": "Inleiding", + "Lines": "Regels", + "Link for staff": "Link voor medewerkers", + "One button under the footer text, such as a link to the contact page. Left out when it has no label or no destination.": "Eén knop onder de tekst in de voettekst, bijvoorbeeld naar de contactpagina. Weggelaten als de tekst of de bestemming ontbreekt.", + "One line more about it.": "Eén regel meer erover.", + "One or two sentences under the heading: who signs in how.": "Een of twee zinnen onder de kop: wie logt hoe in.", + "Points": "Punten", + "Search in the header": "Zoeken in de kop", + "Search page": "Zoekpagina", + "Show the search box": "Zoekvak tonen", + "Side panel": "Zijvak", + "Sign-in cards": "Inlogkaarten", + "Sign-in page": "Inlogpagina", + "The heading above the lines, such as Contact.": "De kop boven de regels, bijvoorbeeld Contact.", + "The heading of the box.": "De kop van het vak.", + "The heading of the panel.": "De kop van het vak.", + "The lines of the column, in order.": "De regels van de kolom, op volgorde.", + "The name of an icon for the card. DigiD and eHerkenning show their own mark.": "De naam van een pictogram voor de kaart. DigiD en eHerkenning tonen hun eigen logo.", + "The name of an icon for the point.": "De naam van een pictogram voor het punt.", + "The page heading, such as Inloggen bij Mijn Wilgenboom. Empty shows Welkom.": "De kop van de pagina, bijvoorbeeld Inloggen bij Mijn Wilgenboom. Leeg toont Welkom.", + "The page of this portal that shows the results, such as /zoeken. Empty or not a page of this portal means /zoeken.": "De pagina van dit portaal die de resultaten toont, bijvoorbeeld /zoeken. Leeg of geen pagina van dit portaal betekent /zoeken.", + "The point in a few words.": "Het punt in een paar woorden.", + "The points in the panel.": "De punten in het vak.", + "The sentence before the link, such as Werkt u bij De Wilgenboom?": "De zin voor de link, bijvoorbeeld Werkt u bij De Wilgenboom?", + "The short text in the empty box, which is also its name for screen readers. Empty shows \"Zoeken\".": "De korte tekst in het lege vak, die ook de naam voor schermlezers is. Leeg toont \"Zoeken\".", + "The text around the sign-in cards. Every part is optional.": "De tekst rond de inlogkaarten. Elk deel mag leeg blijven.", + "The text of one button in the header that opens the visitor's own area, where they choose how to sign in. Empty shows a link for each way to sign in instead.": "De tekst van één knop in de kop die de eigen omgeving van de bezoeker opent, waar die kiest hoe hij inlogt. Leeg toont in plaats daarvan een link voor elke manier van inloggen.", + "The text of the box.": "De tekst van het vak.", + "The text of the card for each way to sign in on the sign-in page, keyed by mode (digid, nextcloud, eherkenning, oidc, local, eidas). A mode without a card shows one plain button.": "De tekst van de kaart voor elke manier van inloggen op de inlogpagina, per manier (digid, nextcloud, eherkenning, oidc, local, eidas). Een manier zonder kaart toont één gewone knop.", + "The text of the link.": "De tekst van de link.", + "The text of the sign-in button. Empty shows the standard text for this way in.": "De tekst van de inlogknop. Leeg toont de gewone tekst voor deze manier van inloggen.", + "What the line says.": "Wat de regel zegt.", + "What the visitor can do after signing in this way.": "Wat de bezoeker kan doen na het inloggen op deze manier.", + "Where the line links to: a page of this portal, or a web, mail or phone address. Empty shows plain text.": "Waar de regel naar linkt: een pagina van dit portaal, of een web-, mail- of telefoonadres. Leeg toont gewone tekst.", + "Where the link goes.": "Waar de link naartoe gaat.", + "Where the link goes: a page of this portal such as /contact, or a full web, mail or phone address.": "Waar de link naartoe gaat: een pagina van dit portaal zoals /contact, of een volledig web-, mail- of telefoonadres.", + "Whether the header shows the search box.": "Of de kop het zoekvak toont.", + "Who this way in is for, such as Ik ben leerling.": "Voor wie deze manier van inloggen is, bijvoorbeeld Ik ben leerling.", + "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. A portal that declares it shows it, unless it switches it off.": "Een zoekvak in de kop, op elke pagina. Het opent de zoekpagina van het portaal met wat de bezoeker typte. Een portaal dat het opgeeft toont het, tenzij het uit staat.", + "False hides a declared search box. Empty shows it.": "Uit verbergt een opgegeven zoekvak. Leeg toont het.", + "Name for screen readers": "Naam voor schermlezers", + "What a screen reader calls the box, such as Zoeken op de website. Empty uses the hint.": "Hoe een schermlezer het vak noemt, bijvoorbeeld Zoeken op de website. Leeg gebruikt de hint.", + "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks).": "Op een lijstblok: kaarten, rijen met datum, balken of cijferlabels in plaats van een tabel; bij taken de uitgelichte kaart; bij kerncijfers één verdeelde balk; bij de kalender datumtegels (site-school-blocks).", + "On a greeting: whether today's date shows; on unless false.": "Bij een begroeting: of de datum van vandaag zichtbaar is; aan tenzij uit.", + "On a segmented kpi: a fixed whole.": "Bij een verdeeld kerncijfer: een vast totaal.", + "On a segmented kpi: the field with the whole.": "Bij een verdeeld kerncijfer: het veld met het totaal.", + "On a segmented kpi: the unit after the figure.": "Bij een verdeeld kerncijfer: de eenheid na het getal.", + "On a segmented kpi: up to five parts, each field, label and tone.": "Bij een verdeeld kerncijfer: hoogstens vijf delen, elk met veld, label en toon.", + "On bars and chips: the label of each row.": "Bij balken en cijferlabels: het label van elke rij.", + "On bars: the figure a full bar stands for.": "Bij balken: het getal dat een volle balk betekent.", + "On bars: the figure of each row.": "Bij balken: het getal van elke rij.", + "On bars: the grey head line, from the first row.": "Bij balken: de grijze kopregel, uit de eerste rij.", + "On bars: the small heading above the note.": "Bij balken: de kleine kop boven de notitie.", + "On cards and highlight tasks: the fields of the second line.": "Bij kaarten en uitgelichte taken: de velden van de tweede regel.", + "On cards: the coming-up part.": "Bij kaarten: het deel dat binnenkort komt.", + "On cards: the first letter of the title in a circle.": "Bij kaarten: de eerste letter van de titel in een cirkel.", + "On cards: the small heading above the coming-up part.": "Bij kaarten: de kleine kop boven het deel dat binnenkort komt.", + "On cards: the text beside the status; on bars: the note under the bars, from the first row.": "Bij kaarten: de tekst naast de status; bij balken: de notitie onder de balken, uit de eerste rij.", + "On chips: a mark below this is marked as below the pass mark.": "Bij cijferlabels: een cijfer hieronder krijgt het teken onvoldoende.", + "On chips: the average of each row.": "Bij cijferlabels: het gemiddelde van elke rij.", + "On chips: the list of marks of each row.": "Bij cijferlabels: de lijst met cijfers van elke rij.", + "On highlight tasks: the small uppercase label.": "Bij uitgelichte taken: het kleine label in hoofdletters.", + "On highlight tasks: the words on the button.": "Bij uitgelichte taken: de tekst op de knop.", + "On rows and cards: the field shown as a status pill.": "Bij rijen en kaarten: het veld dat als statuslabel verschijnt.", + "On rows and cards: the tone of each status value (neutral, success, warning, error).": "Bij rijen en kaarten: de toon van elke statuswaarde (neutraal, gelukt, waarschuwing, fout).", + "On rows: the field drawn as a date tile.": "Bij rijen: het veld dat als datumtegel verschijnt.", + "On rows: the line shown in quotes.": "Bij rijen: de regel tussen aanhalingstekens.", + "On rows: the line under the status pill.": "Bij rijen: de regel onder het statuslabel.", + "On rows: the second line.": "Bij rijen: de tweede regel.", + "On the website": "Op de website", + "The field with the date or moment before which a row is not shown, in a list, an inbox or a read by id, on the server clock (site-school-blocks).": "Het veld met de datum of het moment waarvoor een rij niet zichtbaar is, in een lijst, een postvak of een opvraging per id, volgens de klok van de server (site-school-blocks).", + "The slug of the portal whose public website shows this item when `public` is on. Empty means no website shows it.": "De slug van het portaal waarvan de openbare website dit bericht toont als `public` aan staat. Leeg betekent dat geen website het toont.", + "The words the public website shows beside the date to say who the item is for, such as 'groep 7' or 'hele school'. Written by staff, because the site never reads the school app's groups for a visitor who is not signed in. Empty shows nothing.": "De woorden die de openbare website naast de datum toont om te zeggen voor wie het bericht is, zoals 'groep 7' of 'hele school'. Geschreven door medewerkers, omdat de site de groepen van de school niet leest voor een bezoeker die niet is ingelogd. Leeg toont niets.", + "Visible from field": "Veld zichtbaar vanaf", + "Website": "Website", + "Whether the item also shows on the public website of `portal`, for visitors who are not signed in. Off by default: a news item is for its audience only until staff turn this on. An item that targets single children never shows publicly, whatever this says (PublicNewsReader).": "Of het bericht ook op de openbare website van `portal` staat, voor bezoekers die niet zijn ingelogd. Standaard uit: een bericht is alleen voor de eigen doelgroep tot medewerkers dit aanzetten. Een bericht voor losse kinderen staat nooit openbaar, wat hier ook staat (PublicNewsReader).", + "Who it is for, on the website": "Voor wie, op de website", + "avatar": "letter in een cirkel", + "averageField": "veld met het gemiddelde", + "buttonLabel": "tekst op de knop", + "captionField": "veld met de kopregel", + "dateField": "veld met de datum", + "eyebrow": "label boven de titel", + "labelField": "veld met het label", + "lowBelow": "onvoldoende onder", + "max": "maximum", + "noteField": "veld met de notitie", + "noteLabel": "kop boven de notitie", + "quoteField": "veld met het citaat", + "segments": "delen", + "showDate": "datum tonen", + "soonField": "veld met wat binnenkort komt", + "soonLabel": "kop boven wat binnenkort komt", + "statusField": "veld met de status", + "statusNoteField": "veld met de statusnotitie", + "statusTones": "tonen per status", + "subtitleField": "veld met de tweede regel", + "subtitleFields": "velden van de tweede regel", + "target": "doel", + "totalField": "veld met het totaal", + "unit": "eenheid", + "valueField": "veld met de waarde", + "valuesField": "veld met de waarden" }, "plurals": "", "pluralForm": "nplurals=2; plural=(n != 1);" diff --git a/lib/Controller/PortalPageController.php b/lib/Controller/PortalPageController.php index 3f6bf794d..225728d88 100644 --- a/lib/Controller/PortalPageController.php +++ b/lib/Controller/PortalPageController.php @@ -535,13 +535,15 @@ private function siteThemeStylesheet(): string { * an unthemed portal would quietly restyle a page that must render * unstyled, and an unthemed page names no bundled family. * - * @return array{bridge: string, fonts: string} Paths relative to the theme app's `css/`. + * @return array{bridge: string, fonts: string, logoInverse: string, emblem: string} The stylesheets + * (relative to the theme app's `css/`) and the two logo variants (absolute). * * @spec openspec/changes/site-links-the-theme-bridge/specs/portaliq-cms/spec.md#requirement-the-site-must-link-the-theme-apps-public-bridge-before-a-resolved-token-set-req-stb-001 * @spec openspec/changes/site-links-the-theme-bridge/specs/portaliq-cms/spec.md#requirement-the-site-must-link-the-faces-the-theme-app-bundles-req-stb-002 + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-footer-must-carry-the-motif-the-light-logo-and-the-brand-column-first */ private function siteThemeAppSheets(): array { - $none = ['bridge' => '', 'fonts' => '']; + $none = ['bridge' => '', 'fonts' => '', 'logoInverse' => '', 'emblem' => '']; if ($this->siteThemeStylesheet() === '') { return $none; } @@ -551,6 +553,10 @@ private function siteThemeAppSheets(): array { return [ 'bridge' => (string)($shipped['bridge'] ?? ''), 'fonts' => (string)($shipped['fonts'] ?? ''), + // The set's light logo for the dark footer and its emblem for + // a watermark, absolute, or '' (site-chrome-follows-the-design). + 'logoInverse' => $this->siteThemeLogoUrl(variant: 'dark'), + 'emblem' => $this->siteThemeLogoUrl(variant: 'emblem'), ]; } catch (\Throwable) { return $none; @@ -576,11 +582,19 @@ private function siteThemeAppSheets(): array { * So the resolution happens here, where the theme app's real path is * known, and the template emits the result after the token stylesheets. * + * With a `$variant` it is that variant of the set's logo + * (site-chrome-follows-the-design): `dark`, the light logo for the dark + * footer band, or `emblem`, the mark for a watermark; '' when the set + * ships none. + * + * @param string $variant '' for the logo, else `dark` or `emblem`. + * * @return string An absolute URL, or '' when there is no logo to serve. * * @spec openspec/specs/portaliq-cms/spec.md#requirement-a-portals-theme-must-change-what-a-visitor-sees + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-footer-must-carry-the-motif-the-light-logo-and-the-brand-column-first */ - private function siteThemeLogoUrl(): string { + private function siteThemeLogoUrl(string $variant=''): string { $stylesheet = $this->siteThemeStylesheet(); if ($stylesheet === '') { return ''; @@ -593,7 +607,8 @@ private function siteThemeLogoUrl(): string { } try { - $relative = $this->themeResolver->logoFileFor(theme: $theme); + $relative = $this->themeResolver->logoFileFor(theme: $theme, variant: $variant); + if ($relative === null) { return ''; } diff --git a/lib/Service/Cms/PortalShell.php b/lib/Service/Cms/PortalShell.php index b54e86014..8e91c42ce 100644 --- a/lib/Service/Cms/PortalShell.php +++ b/lib/Service/Cms/PortalShell.php @@ -39,13 +39,16 @@ class PortalShell { */ public const HEADER_VARIANTS = ['double', 'single']; + /** * Constructor. * - * @param PortalRegionResolver $regions The closed list of regions. + * @param PortalRegionResolver $regions The closed list of regions. + * @param PortalSignInText $signInText The sign-in page's text. */ public function __construct( private readonly PortalRegionResolver $regions=new PortalRegionResolver(), + private readonly PortalSignInText $signInText=new PortalSignInText(), ) { }//end __construct() @@ -54,19 +57,57 @@ public function __construct( * * @param array $portal The portal record. * - * @return array `authentication`, `headerVariant`, `footer` and `regions`. + * @return array `authentication`, `headerVariant`, `headerSearch`, `accountLabel`, `footer` and `regions`. * * @spec openspec/changes/portal-theme-blocks-and-contributed-pages/specs/portaliq-cms/spec.md#requirement-the-header-must-be-a-block-whose-shape-the-portal-chooses-req-ptb-004 + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-header-must-carry-the-search-box-and-one-way-to-the-own-area */ public function project(array $portal): array { return [ 'authentication' => $this->authentication(portal: $portal), 'headerVariant' => $this->headerVariant(portal: $portal), + 'headerSearch' => $this->headerSearch(portal: $portal), + 'accountLabel' => $this->text(value: ($portal['accountLabel'] ?? '')), 'footer' => $this->footer(portal: $portal), 'regions' => $this->publicRegions(portal: $portal), ]; }//end project() + /** + * The search box in the header: whether it shows (a declared box shows + * unless `enabled` is false), its accessible name, its hint and the + * portal's search page. A route that is not an in-site path falls back to + * `/zoeken`, the page the hero search has always opened. + * + * @param array $portal The portal record. + * + * @return array{enabled: bool, label: string, placeholder: string, route: string} The box. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-header-must-carry-the-search-box-and-one-way-to-the-own-area + */ + public function headerSearch(array $portal): array { + $search = $portal['headerSearch'] ?? []; + if (is_array($search) === false) { + $search = []; + } + + $route = $this->text(value: ($search['route'] ?? '')); + if (preg_match('#^/(?!/)#', $route) !== 1) { + $route = '/zoeken'; + } + + // Declared is on: a portal that writes the box wants it, unless it + // says `enabled: false` (lane L3 declares `label` and `route` only). + $declared = (is_array($portal['headerSearch'] ?? null) === true && $search !== []); + + return [ + 'enabled' => $declared === true && ($search['enabled'] ?? true) === true, + 'label' => $this->text(value: ($search['label'] ?? '')), + 'placeholder' => $this->text(value: ($search['placeholder'] ?? '')), + 'route' => $route, + ]; + }//end headerSearch() + /** * The header variant: the portal's choice when it is known, else `double`. * @@ -96,6 +137,7 @@ public function headerVariant(array $portal): string { * @return array `{modes, register?, registerLabel?}`. * * @spec openspec/changes/portal-theme-blocks-and-contributed-pages/specs/portaliq-cms/spec.md#requirement-the-header-must-be-a-block-whose-shape-the-portal-chooses-req-ptb-004 + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role */ public function authentication(array $portal): array { $auth = (array)($portal['authentication'] ?? []); @@ -108,7 +150,7 @@ public function authentication(array $portal): array { } } - return $public; + return $public + $this->signInText->project(auth: $auth); }//end authentication() /** @@ -120,9 +162,10 @@ public function authentication(array $portal): array { * * @param array $portal The portal record. * - * @return array `{description, colophon, socials, legalLinks, badges}`; each list holds `{label, href}` entries. + * @return array `{description, colophon, socials, legalLinks, badges, cta, contact}`; each list holds `{label, href}` entries. * * @spec openspec/changes/portal-theme-blocks-and-contributed-pages/specs/portaliq-cms/spec.md#requirement-the-footer-must-be-a-block-whose-bands-are-styled-by-role-req-ptb-005 + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-footer-must-carry-the-motif-the-light-logo-and-the-brand-column-first */ public function footer(array $portal): array { $footer = $portal['footer'] ?? []; @@ -130,15 +173,61 @@ public function footer(array $portal): array { $footer = []; } + $cta = $this->links(entries: [($footer['cta'] ?? null)], extra: null); + return [ 'description' => $this->text(value: ($footer['description'] ?? '')), 'colophon' => $this->text(value: ($footer['colophon'] ?? '')), 'socials' => $this->links(entries: ($footer['socials'] ?? []), extra: 'icon'), 'legalLinks' => $this->links(entries: ($footer['legalLinks'] ?? []), extra: null), 'badges' => $this->links(entries: ($footer['badges'] ?? []), extra: null), + 'cta' => ($cta[0] ?? null), + 'contact' => $this->contact(contact: ($footer['contact'] ?? null)), ]; }//end footer() + /** + * The footer's contact column: a title and plain lines, a line with a + * followable `href` rendered as a link. Null when it has no line. + * + * @param mixed $contact The authored column. + * + * @return array{title: string, lines: list>}|null The column. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-footer-must-carry-the-motif-the-light-logo-and-the-brand-column-first + */ + private function contact(mixed $contact): ?array { + if (is_array($contact) === false) { + return null; + } + + $lines = []; + foreach ((array)($contact['lines'] ?? []) as $line) { + if (is_array($line) === false) { + continue; + } + + $text = $this->text(value: ($line['text'] ?? '')); + if ($text === '') { + continue; + } + + $kept = ['text' => $text]; + $href = $this->text(value: ($line['href'] ?? '')); + if ($this->followable(href: $href) === true) { + $kept['href'] = $href; + } + + $lines[] = $kept; + } + + if ($lines === []) { + return null; + } + + return ['title' => $this->text(value: ($contact['title'] ?? '')), 'lines' => $lines]; + }//end contact() + /** * The portal's own region contents, the middle step of resolution. * diff --git a/lib/Service/Cms/PortalSignInText.php b/lib/Service/Cms/PortalSignInText.php new file mode 100644 index 000000000..68b4cd45e --- /dev/null +++ b/lib/Service/Cms/PortalSignInText.php @@ -0,0 +1,219 @@ + + * SPDX-License-Identifier: EUPL-1.2 + * + * @category Service + * @package OCA\Portaliq\Service\Cms + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ + +declare(strict_types=1); + +namespace OCA\Portaliq\Service\Cms; + +/** + * Projects `authentication.modeLabels` and `authentication.signInPage` on + * named keys, plain text only, a link only when it can be followed. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ +class PortalSignInText { + + /** + * The ways in a sign-in card may be written for (the portal's modes + * besides `public`). + */ + public const MODES = ['nextcloud', 'local', 'oidc', 'digid', 'eherkenning', 'eidas']; + + /** + * The sign-in keys of an authentication block that say something. + * + * @param array $auth The portal's authentication block. + * + * @return array `modeLabels` and `signInPage`, each only when not empty. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ + public function project(array $auth): array { + $public = []; + $labels = $this->modeLabels(labels: ($auth['modeLabels'] ?? [])); + if ($labels !== []) { + $public['modeLabels'] = $labels; + } + + $page = $this->signInPage(page: ($auth['signInPage'] ?? [])); + if ($page !== []) { + $public['signInPage'] = $page; + } + + return $public; + }//end project() + + /** + * The sign-in card per way in: title, text, button, hint and icon, each + * plain text. A mode the portal could not offer is dropped; so is a card + * that says nothing. + * + * @param mixed $labels The authored map, mode to card. + * + * @return array> Mode to `{title?, text?, button?, hint?, icon?}`. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ + private function modeLabels(mixed $labels): array { + if (is_array($labels) === false) { + return []; + } + + $kept = []; + foreach (self::MODES as $mode) { + $card = $labels[$mode] ?? null; + if (is_array($card) === false) { + continue; + } + + $texts = $this->texts(source: $card, keys: ['title', 'text', 'button', 'hint', 'icon']); + if ($texts !== []) { + $kept[$mode] = $texts; + } + } + + return $kept; + }//end modeLabels() + + /** + * The text around the sign-in cards: the page's title and intro, a + * notice under the cards, a line for staff with its link, and a side + * panel of points. Plain text; a link only when it can be followed. + * + * @param mixed $page The authored block. + * + * @return array The parts that say something. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ + private function signInPage(mixed $page): array { + if (is_array($page) === false) { + return []; + } + + $kept = $this->texts(source: $page, keys: ['title', 'intro']); + + $notice = $this->texts(source: ($page['notice'] ?? null), keys: ['title', 'text']); + if (($notice['text'] ?? '') !== '') { + $kept['notice'] = $notice; + } + + $staff = $this->texts(source: ($page['staffLink'] ?? null), keys: ['text', 'label', 'href']); + if (($staff['label'] ?? '') !== '' && $this->followable(href: ($staff['href'] ?? '')) === true) { + $kept['staffLink'] = $staff; + } + + $panel = $this->panel(panel: ($page['panel'] ?? null)); + if ($panel !== null) { + $kept['panel'] = $panel; + } + + return $kept; + }//end signInPage() + + /** + * The side panel: a title and the points that have a title. Null when + * it says nothing. + * + * @param mixed $panel The authored panel. + * + * @return array{title: string, items: list>}|null The panel. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ + private function panel(mixed $panel): ?array { + if (is_array($panel) === false) { + return null; + } + + $items = []; + foreach ((array)($panel['items'] ?? []) as $item) { + $texts = $this->texts(source: $item, keys: ['title', 'text', 'icon']); + if (($texts['title'] ?? '') !== '') { + $items[] = $texts; + } + } + + $title = $this->text(value: ($panel['title'] ?? '')); + if ($title === '' && $items === []) { + return null; + } + + return ['title' => $title, 'items' => $items]; + }//end panel() + + /** + * The named keys of an authored block that hold text, trimmed. + * + * @param mixed $source The block; anything but an array says nothing. + * @param list $keys The keys to keep. + * + * @return array The keys that say something. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ + private function texts(mixed $source, array $keys): array { + $kept = []; + if (is_array($source) === false) { + return []; + } + + foreach ($keys as $key) { + $value = $this->text(value: ($source[$key] ?? '')); + if ($value !== '') { + $kept[$key] = $value; + } + } + + return $kept; + }//end texts() + + /** + * Whether a visitor can follow this destination: an in-site route or a + * web, mail or phone address. + * + * @param string $href The destination. + * + * @return bool True when it may be rendered as a link. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ + private function followable(string $href): bool { + return preg_match('#^(/(?!/)|https?://|mailto:|tel:)#i', $href) === 1; + }//end followable() + + /** + * A scalar as trimmed text; anything else as empty. + * + * @param mixed $value The authored value. + * + * @return string The text. + * + * @spec openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md#requirement-the-sign-in-page-must-offer-each-way-in-as-a-card-for-its-role + */ + private function text(mixed $value): string { + if (is_scalar($value) === false) { + return ''; + } + + return trim((string)$value); + }//end text() +}//end class diff --git a/lib/Service/PortalThemeResolver.php b/lib/Service/PortalThemeResolver.php index 95e2400be..cb7217aa0 100644 --- a/lib/Service/PortalThemeResolver.php +++ b/lib/Service/PortalThemeResolver.php @@ -87,6 +87,13 @@ class PortalThemeResolver { */ public const FONT_STYLESHEET = 'fonts'; + /** + * The logo variants a page may ask for, with their file suffix. + * + * @var array + */ + private const LOGO_VARIANTS = ['' => '', 'dark' => '-dark', 'emblem' => '-emblem']; + /** * Constructor. @@ -196,13 +203,19 @@ public function shippedStylesheets(): array { * null so the page renders with NO logo instead of a broken image or * another brand's mark — the same posture `stylesheetFor()` takes. * - * @param string $theme The portal's theme reference, e.g. 'opencatalogi'. + * A variant (site-chrome-follows-the-design) is `dark`, the light logo + * for a dark band such as a school portal's footer (`-dark.svg`, + * named after the dark scheme it was drawn for), or `emblem`, the mark + * without the name for a faint watermark (`-emblem.svg`). + * + * @param string $theme The portal's theme reference, e.g. 'opencatalogi'. + * @param string $variant '' for the logo, else `dark` or `emblem`. * * @return string|null The path relative to the theme app, or null. * * @spec openspec/specs/portaliq-cms/spec.md#requirement-a-portals-theme-must-change-what-a-visitor-sees */ - public function logoFileFor(string $theme): ?string { + public function logoFileFor(string $theme, string $variant=''): ?string { if ($this->stylesheetFor(theme: $theme) === null) { return null; } @@ -212,7 +225,9 @@ public function logoFileFor(string $theme): ?string { return null; } - $relative = 'img/logos/' . $theme . '.svg'; + // An unknown variant names a path no theme app ships, so it ends in + // the missing-file answer below. + $relative = 'img/logos/' . $theme . (self::LOGO_VARIANTS[$variant] ?? '/-') . '.svg'; if (is_file($root . '/' . $relative) === false) { return null; } diff --git a/lib/Settings/portaliq_register.json b/lib/Settings/portaliq_register.json index 9944743cc..e81d27c22 100644 --- a/lib/Settings/portaliq_register.json +++ b/lib/Settings/portaliq_register.json @@ -3,7 +3,7 @@ "info": { "title": "Portaliq Register", "description": "Portaliq — the fleet's shared external portal for clients and suppliers (ADR-046). Holds every portal account, page, session, submission, message, notification, audit entry, traffic record, form and landing-page submission the app reads and writes.", - "version": "0.64.0" + "version": "0.65.0" }, "x-openregister": { "type": "application", @@ -20,7 +20,7 @@ "slug": "portaliq", "title": "Portaliq Register", "description": "Portaliq — the fleet's shared external portal for clients and suppliers (ADR-046). Holds every portal account, page, session, submission, message, notification, audit entry and traffic record the app reads and writes.", - "version": "0.64.0", + "version": "0.65.0", "schemas": [ "exampleDocument", "glossaryTerm", @@ -2442,7 +2442,7 @@ "portal": { "slug": "portal", "icon": "Web", - "version": "0.9.0", + "version": "0.10.0", "title": "Portal", "authorization": { "read": [ @@ -2550,6 +2550,56 @@ } } } + }, + "cta": { + "title": "Footer button", + "type": "object", + "description": "One button under the footer text, such as a link to the contact page. Left out when it has no label or no destination.", + "properties": { + "label": { + "title": "Label", + "type": "string", + "description": "The text a visitor reads or a screen reader announces." + }, + "href": { + "title": "Link", + "type": "string", + "description": "Where the link goes: a page of this portal such as /contact, or a full web, mail or phone address." + } + } + }, + "contact": { + "title": "Contact column", + "type": "object", + "description": "A column of contact details beside the footer text: an address, a phone number, an e-mail address. A line with a link is shown as a link.", + "properties": { + "title": { + "title": "Column heading", + "type": "string", + "description": "The heading above the lines, such as Contact.", + "example": "Contact" + }, + "lines": { + "title": "Lines", + "type": "array", + "description": "The lines of the column, in order.", + "items": { + "type": "object", + "properties": { + "text": { + "title": "Text", + "type": "string", + "description": "What the line says." + }, + "href": { + "title": "Link", + "type": "string", + "description": "Where the line links to: a page of this portal, or a web, mail or phone address. Empty shows plain text." + } + } + } + } + } } } }, @@ -2922,6 +2972,43 @@ ], "example": "double" }, + "headerSearch": { + "title": "Search in the header", + "type": "object", + "description": "A search box in the header, on every page. It opens the portal's search page with what the visitor typed. A portal that declares it shows it, unless it switches it off.", + "properties": { + "enabled": { + "title": "Show the search box", + "type": "boolean", + "description": "False hides a declared search box. Empty shows it.", + "example": true + }, + "label": { + "title": "Name for screen readers", + "type": "string", + "description": "What a screen reader calls the box, such as Zoeken op de website. Empty uses the hint.", + "example": "Zoeken op de website" + }, + "placeholder": { + "title": "Hint in the box", + "type": "string", + "description": "The short text in the empty box, which is also its name for screen readers. Empty shows \"Zoeken\".", + "example": "Zoek een cursus" + }, + "route": { + "title": "Search page", + "type": "string", + "description": "The page of this portal that shows the results, such as /zoeken. Empty or not a page of this portal means /zoeken.", + "example": "/zoeken" + } + } + }, + "accountLabel": { + "title": "Button to the own area", + "type": "string", + "description": "The text of one button in the header that opens the visitor's own area, where they choose how to sign in. Empty shows a link for each way to sign in instead.", + "example": "Mijn Wilgenboom" + }, "locales": { "title": "Locales", "type": "array", @@ -3057,6 +3144,134 @@ "type": "string", "description": "The text on the register button. Empty shows Registreren.", "example": "Account aanmaken" + }, + "modeLabels": { + "title": "Sign-in cards", + "type": "object", + "description": "The text of the card for each way to sign in on the sign-in page, keyed by mode (digid, nextcloud, eherkenning, oidc, local, eidas). A mode without a card shows one plain button.", + "additionalProperties": { + "type": "object", + "properties": { + "title": { + "title": "Card heading", + "type": "string", + "description": "Who this way in is for, such as Ik ben leerling." + }, + "text": { + "title": "Card text", + "type": "string", + "description": "What the visitor can do after signing in this way." + }, + "button": { + "title": "Button text", + "type": "string", + "description": "The text of the sign-in button. Empty shows the standard text for this way in." + }, + "hint": { + "title": "Hint", + "type": "string", + "description": "A short line under the button, such as what you need to sign in." + }, + "icon": { + "title": "Icon", + "type": "string", + "description": "The name of an icon for the card. DigiD and eHerkenning show their own mark." + } + } + } + }, + "signInPage": { + "title": "Sign-in page", + "type": "object", + "description": "The text around the sign-in cards. Every part is optional.", + "properties": { + "title": { + "title": "Heading", + "type": "string", + "description": "The page heading, such as Inloggen bij Mijn Wilgenboom. Empty shows Welkom." + }, + "intro": { + "title": "Introduction", + "type": "string", + "description": "One or two sentences under the heading: who signs in how." + }, + "notice": { + "title": "Notice", + "type": "object", + "description": "A box under the cards, for example what to do the first time.", + "properties": { + "title": { + "title": "Heading", + "type": "string", + "description": "The heading of the box." + }, + "text": { + "title": "Text", + "type": "string", + "description": "The text of the box." + } + } + }, + "staffLink": { + "title": "Link for staff", + "type": "object", + "description": "A line under the cards for staff, with a link to where they sign in.", + "properties": { + "text": { + "title": "Text", + "type": "string", + "description": "The sentence before the link, such as Werkt u bij De Wilgenboom?" + }, + "label": { + "title": "Link text", + "type": "string", + "description": "The text of the link." + }, + "href": { + "title": "Link", + "type": "string", + "description": "Where the link goes." + } + } + }, + "panel": { + "title": "Side panel", + "type": "object", + "description": "A panel beside the cards with what the visitor finds after signing in.", + "properties": { + "title": { + "title": "Heading", + "type": "string", + "description": "The heading of the panel." + }, + "items": { + "title": "Points", + "type": "array", + "description": "The points in the panel.", + "items": { + "type": "object", + "properties": { + "title": { + "title": "Heading", + "type": "string", + "description": "The point in a few words." + }, + "text": { + "title": "Text", + "type": "string", + "description": "One line more about it." + }, + "icon": { + "title": "Icon", + "type": "string", + "description": "The name of an icon for the point." + } + } + } + } + } + } + } } } }, diff --git a/openspec/changes/site-chrome-follows-the-design/proposal.md b/openspec/changes/site-chrome-follows-the-design/proposal.md new file mode 100644 index 000000000..a34ca4869 --- /dev/null +++ b/openspec/changes/site-chrome-follows-the-design/proposal.md @@ -0,0 +1,53 @@ +## Why + +Four school portals (De Wilgenboom, Vaartveld College, Esdoornveen, Warmtepompacademie) and the +Zuiddrecht municipality have finished designs for the website's header, footer and sign-in page. +The site could not draw them: no motif under the header or over the footer, no search box in the +header, a sign-in link per mode instead of one button to the own area, a bare signed-in name, no +button or contact column in the footer, the light logo missing on the dark footer, and a sign-in +page that is one heading and a row of buttons. A portal switched to a school theme looked worse +than on the old `example-*` set (closed in thematiq `brand-motif-on-portals`, which this change +reads). + +Gaps G-01, G-02, G-03, G-04, G-07, G-08 and G-18 of the school portal plan. + +## What Changes + +- **The motif** under the header and over the footer, as CSS on existing elements + (`css/site-theme.css`), from the theme's `--cn-brand-stripe-*` tokens. Nothing is drawn for a set + without a stripe. Under the header a set without a motif image draws one line in its first + stripe colour (Zuiddrecht's red line); over the footer the inverse image, else the three bands. +- **The designed header**, for a portal that declares `headerSearch.enabled` or `accountLabel`: a + search box that opens the portal's search page, one button to the own area while signed out, a + person chip (initials, name, the organisation from `session.organisationName`) and a sign-out + link while signed in, and on a phone a menu button that opens the menu bar and the search box. + Loaded on demand (`components/chrome/HeaderTools.vue`). Other portals keep their header markup. +- **The current menu item** is bold with a 4px bar in the theme's accent (the primary for a set + without one). +- **The designed footer**, for a portal whose footer declares `cta` or `contact`: brand column + first (light logo, text, outlined button), then the contact column, then the menu columns; the + bottom band one step darker than the footer. Other footers keep their order. +- **Logo variants**: `site.php` names the set's light logo (`img/logos/-dark.svg`) and emblem + (`-emblem.svg`) as absolute addresses (`--nldesign-logo-inverse-url`, `--nldesign-emblem-url`). +- **The hero** takes a light ground, dark type, a filled first button and the emblem as a faint + watermark when the theme says so. +- **The sign-in page as role cards** (`components/chrome/SignInPage.vue`, on demand) for a portal + that writes `authentication.modeLabels` or `authentication.signInPage`: a card per way in (who it + is for, what it opens, the button, a hint), a notice, a line for staff, and a side panel with the + motif along its top. +- **Portal schema and content API**: `headerSearch`, `accountLabel`, `footer.cta`, + `footer.contact`, `authentication.modeLabels`, `authentication.signInPage`, projected on named + keys by `PortalShell` (links only when they can be followed). +- **For lane L2**: `gridContext()` hands the portal's sign-in routes to the widget grid + (`signInRoutes`), for the `nlSignIn` block. + +## Impact + +- Additive and opt-in per portal: a portal that declares none of the new keys renders as before, + except that a set with a stripe now draws it on the site and the current menu item gets its bar. +- Bundle: the new header tools and the sign-in page are lazy chunks; the entry gains the opt-in + wiring only. The production entry size is measured by CI (a local production build is not run + on this machine). +- Deviations, accepted in the plan: the slanted cut of Esdoornveen aligns to the viewport edge, + not the content column (D-1); the participant's e-mail link of the Warmtepompacademie is shown + as the account way in until a magic link exists (D-6). diff --git a/openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md b/openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md new file mode 100644 index 000000000..fa6af9b38 --- /dev/null +++ b/openspec/changes/site-chrome-follows-the-design/specs/site-chrome/spec.md @@ -0,0 +1,101 @@ +## ADDED Requirements + +### Requirement: The header must carry the search box and one way to the own area + +A portal that declares `headerSearch.enabled` or `accountLabel` MUST get the designed header: a +search box with the portal's hint as its visible text and accessible name, submitting to the +portal's search page (`headerSearch.route`, `/zoeken` unless an in-site path is named); while +signed out one button with `accountLabel` that opens `/mijn`; while signed in a chip with the +person's initials, name and organisation linking to the own area, and a sign-out control. On a +phone one menu button MUST open the menu bar and the search box and say whether it is open. A +portal that declares neither MUST keep its header markup unchanged. + +#### Scenario: Signed out +@e2e exclude Rendered in node: tests/site-chrome.spec.mjs; live screenshots under the lane folder +- GIVEN a portal with `headerSearch.enabled` and `accountLabel` "Mijn academie" +- WHEN a visitor opens any page signed out +- THEN the header shows a search box named "Zoek een cursus" and one button "Mijn academie" + +#### Scenario: Signed in +@e2e exclude Rendered in node: tests/site-chrome.spec.mjs +- GIVEN the same portal and a session for Linda Jansen of Jansen Installatietechniek +- WHEN the header renders +- THEN it shows "LJ", "Linda Jansen", "Jansen Installatietechniek" and a sign-out control, and no account button + +#### Scenario: Nothing declared +@e2e exclude Markup compared in node: tests/site-shell-blocks.spec.mjs (baseline header) +- GIVEN a portal without `headerSearch` and `accountLabel` +- WHEN the header renders +- THEN its markup equals the header before this change + +#### Scenario: The content API serves the header keys +@e2e exclude PHPUnit tests/Unit/Service/Cms/PortalShellTest.php +- GIVEN a portal record with `headerSearch` and `accountLabel` +- WHEN `/api/content/site` is read +- THEN `headerSearch` has `enabled` true only for a boolean true, and a route that is not an in-site path reads `/zoeken` + +### Requirement: The motif must run under the header and over the footer + +When the theme hands the site a brand stripe (`--cn-brand-stripe-height`), the site MUST draw it +under the header's menu bar and along the top of the footer. Under the header it MUST draw the +motif image, else one line in the first stripe colour; over the footer the inverse image, else the +image, else the three bands in their ratio. Without a stripe height nothing MUST be drawn. The +current menu item MUST be bold with a bar in `--thematiq-accent-color`. + +#### Scenario: A school motif +@e2e exclude Live screenshot per set under the lane folder; CSS contract in tests/site-chrome.spec.mjs +- GIVEN a portal on `wilgenboom` +- WHEN a page renders +- THEN the twigs run under the menu bar and the light twigs over the footer + +#### Scenario: Zuiddrecht +@e2e exclude Live screenshot under the lane folder +- GIVEN a portal on `zuiddrecht` +- WHEN a page renders +- THEN a red line runs under the menu bar and red, blue, red bands 6 : 3 : 1 over the footer + +### Requirement: The footer must carry the motif, the light logo and the brand column first + +A footer that declares `cta` or `contact` MUST read brand column, contact column, menu columns. +The brand column MUST show the set's light logo when the set ships one, the footer text and the +button. A contact line with a followable `href` MUST be a link, any other line plain text. The +bottom band MUST take `--thematiq-footer-legal-background-color` when the theme names it. Any +other footer MUST keep its order. + +#### Scenario: A designed footer +@e2e exclude Rendered in node: tests/site-chrome.spec.mjs; PHPUnit PortalShellTest and PortalThemeResolverTest +- GIVEN a portal whose footer has a button and a contact column +- WHEN the footer renders +- THEN the brand column comes first, the contact column second, the menus after them + +#### Scenario: An existing footer +@e2e exclude Rendered in node: tests/site-chrome.spec.mjs +- GIVEN a footer without a button or contact column +- WHEN it renders +- THEN the menus come first and the brand column last, as before + +### Requirement: The sign-in page must offer each way in as a card for its role + +A portal that writes `authentication.modeLabels` or `authentication.signInPage` MUST get a +sign-in page with one card per declared way in, in the order of `modes`: the card's title and +text, one button (the first primary, the others secondary), and its hint; then the notice and the +line for staff; and the side panel when it holds a point. A portal that writes neither MUST keep +the plain list of buttons. + +#### Scenario: Two roles +@e2e exclude Rendered in node: tests/site-chrome.spec.mjs; live screenshot under the lane folder +- GIVEN a portal with modes `nextcloud` and `digid`, each with a card +- WHEN a visitor opens `/mijn` signed out +- THEN two cards show, "Ik ben leerling" first with the primary button + +#### Scenario: Only modes +@e2e exclude Source contract in tests/site-chrome.spec.mjs; tests/site-auth.spec.mjs keeps the plain routes +- GIVEN a portal with only `modes` +- WHEN a visitor opens `/mijn` signed out +- THEN the page shows "Welkom" and one button per way in, as before + +#### Scenario: The content API drops what cannot be offered +@e2e exclude PHPUnit tests/Unit/Service/Cms/PortalShellTest.php +- GIVEN cards for `public` and for a mode with no text +- WHEN `/api/content/site` is read +- THEN neither card is served diff --git a/openspec/changes/site-chrome-follows-the-design/tasks.md b/openspec/changes/site-chrome-follows-the-design/tasks.md new file mode 100644 index 000000000..cfb139587 --- /dev/null +++ b/openspec/changes/site-chrome-follows-the-design/tasks.md @@ -0,0 +1,12 @@ +# Tasks: site chrome follows the design + +- [x] 1. Portal schema keys and the `PortalShell` projection (headerSearch, accountLabel, footer.cta, footer.contact, authentication.modeLabels, authentication.signInPage), schema strings in l10n. +- [x] 2. `site.php` and `PortalThemeResolver`: the light logo and the emblem as absolute addresses. +- [x] 3. Motif under the header and over the footer; current item bar (`css/site-theme.css`). +- [x] 4. Designed header: `chrome/HeaderTools.vue` (lazy), BrandHeader wiring, phone menu. +- [x] 5. Designed footer: brand first, button, contact column, darker bottom band. +- [x] 6. Sign-in page as role cards: `chrome/SignInPage.vue` (lazy), AccountArea. +- [x] 7. Light hero with watermark. +- [x] 8. `gridContext().signInRoutes` for lane L2. +- [x] 9. Tests: `tests/site-chrome.spec.mjs`, `PortalShellTest`, `PortalThemeResolverTest`; existing shell tests updated for the new footer keys. +- [x] 10. Live check on :8091 with the four school sets and zuiddrecht (lane folder screenshots). diff --git a/package.json b/package.json index 490453fb2..c79dc3f5a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "check:register": "node tests/validate-register.js", "check:registry": "node tests/registry.spec.js", "check:json-strict": "node tests/validate-json-strict.js", - "check:specs": "npm run check:json-strict && npm run check:manifest-v2 && npm run check:register && npm run check:registry && npm run check:site-auth && npm run check:federated-search && npm run check:campaign-tracking && npm run check:traffic-client && npm run check:traffic-summary && npm run check:traffic-paths && npm run check:page-traffic && npm run check:open-portal-site && npm run check:shared-instance && npm run check:connection-registry && npm run check:leaf-integrations && npm run check:page-layout-entry && npm run check:collection-table && npm run check:case-timeline && npm run check:schema-form-file-field && npm run check:timed-task && npm run check:row-action && npm run check:translated-message-notice && npm run check:access-requests && npm run check:news-item-translation && npm run check:news-title-and-newsletter-translation && npm run check:newsletter-title-translation && npm run check:intake-entry && npm run check:access-request-asker && npm run check:registered-details && npm run check:branch-choice && npm run check:account-page && npm run check:signing-dialog && npm run check:form-binding-preview && npm run check:open-record && npm run check:message-box-channel && npm run check:portal-live-regions && npm run check:case-withdraw-screen && npm run check:case-documents-screen && npm run check:my-cases-page && npm run check:my-cases-acting-for && npm run check:proposal-queue-leaf && npm run check:portal-theme-choice && npm run check:site-head && npm run check:page-history && npm run check:media-library && npm run check:broker-login && npm run check:portal-signin-settings && npm run check:site-shell-blocks && npm run check:site-grid && npm run check:site-regions && npm run check:page-editor && npm run check:page-editor-regions && npm run check:notices && npm run check:site-edit-mode && npm run check:site-page-tree && npm run check:branch-in-effect && npm run check:idle-session && npm run check:publication-documents && npm run check:woo-entry-points && npm run check:attached-actions && npm run check:my-dossiers && npm run check:staff-account-screens && npm run check:site-signed-in-shell && npm run check:site-resident-menu && npm run check:create-names-its-action && npm run check:site-shared-dossier && npm run check:guest-action-page && npm run check:ways-in-screens && npm run check:visible-when-local && npm run check:schema-form && npm run check:propose-change && npm run check:site-collections && npm run check:collection-loader && npm run check:rich-text && npm run check:tasks-page && npm run check:site-inbox-pages && npm run check:embed-copy && npm run check:embed-height && npm run check:service-worker && npm run check:install-banner && npm run check:embed-frame && npm run check:array-cells && npm run check:collection-groups && npm run check:record-page && npm run check:case-type-portal-header && npm run check:inbox-unread && npm run check:inbox-attachments && npm run check:inbox-open-link && npm run check:inbox-delete && npm run check:value-labels && npm run check:admin-menu-access && npm run check:news-list-refresh && npm run check:site-navigation && npm run check:site-theme-bridge && npm run check:mijn-components && npm run check:mijn-cases && npm run check:mijn-home && npm run check:mijn-lists && npm run check:mijn-wave6 && npm run check:mijn-documents && npm run check:site-form-fields && npm run check:widget-registry && npm run check:action-summary-sentence && npm run check:site-school-blocks && npm run check:site-school-displays && npm run check:widget-palette && npm run check:editor-text-toolbar && npm run check:widget-tokens && npm run check:portal-home-page && npm run check:claim-invitation", + "check:specs": "npm run check:json-strict && npm run check:manifest-v2 && npm run check:register && npm run check:registry && npm run check:site-auth && npm run check:federated-search && npm run check:campaign-tracking && npm run check:traffic-client && npm run check:traffic-summary && npm run check:traffic-paths && npm run check:page-traffic && npm run check:open-portal-site && npm run check:shared-instance && npm run check:connection-registry && npm run check:leaf-integrations && npm run check:page-layout-entry && npm run check:collection-table && npm run check:case-timeline && npm run check:schema-form-file-field && npm run check:timed-task && npm run check:row-action && npm run check:translated-message-notice && npm run check:access-requests && npm run check:news-item-translation && npm run check:news-title-and-newsletter-translation && npm run check:newsletter-title-translation && npm run check:intake-entry && npm run check:access-request-asker && npm run check:registered-details && npm run check:branch-choice && npm run check:account-page && npm run check:signing-dialog && npm run check:form-binding-preview && npm run check:open-record && npm run check:message-box-channel && npm run check:portal-live-regions && npm run check:case-withdraw-screen && npm run check:case-documents-screen && npm run check:my-cases-page && npm run check:my-cases-acting-for && npm run check:proposal-queue-leaf && npm run check:portal-theme-choice && npm run check:site-head && npm run check:page-history && npm run check:media-library && npm run check:broker-login && npm run check:portal-signin-settings && npm run check:site-shell-blocks && npm run check:site-grid && npm run check:site-regions && npm run check:page-editor && npm run check:page-editor-regions && npm run check:notices && npm run check:site-edit-mode && npm run check:site-page-tree && npm run check:branch-in-effect && npm run check:idle-session && npm run check:publication-documents && npm run check:woo-entry-points && npm run check:attached-actions && npm run check:my-dossiers && npm run check:staff-account-screens && npm run check:site-signed-in-shell && npm run check:site-resident-menu && npm run check:create-names-its-action && npm run check:site-shared-dossier && npm run check:guest-action-page && npm run check:ways-in-screens && npm run check:visible-when-local && npm run check:schema-form && npm run check:propose-change && npm run check:site-collections && npm run check:collection-loader && npm run check:rich-text && npm run check:tasks-page && npm run check:site-inbox-pages && npm run check:embed-copy && npm run check:embed-height && npm run check:service-worker && npm run check:install-banner && npm run check:embed-frame && npm run check:array-cells && npm run check:collection-groups && npm run check:record-page && npm run check:case-type-portal-header && npm run check:inbox-unread && npm run check:inbox-attachments && npm run check:inbox-open-link && npm run check:inbox-delete && npm run check:value-labels && npm run check:admin-menu-access && npm run check:news-list-refresh && npm run check:site-navigation && npm run check:site-theme-bridge && npm run check:mijn-components && npm run check:mijn-cases && npm run check:mijn-home && npm run check:mijn-lists && npm run check:mijn-wave6 && npm run check:mijn-documents && npm run check:site-form-fields && npm run check:widget-registry && npm run check:action-summary-sentence && npm run check:site-school-blocks && npm run check:site-school-displays && npm run check:widget-palette && npm run check:editor-text-toolbar && npm run check:widget-tokens && npm run check:portal-home-page && npm run check:claim-invitation && npm run check:site-chrome", "prepare": "git config core.hooksPath .githooks || true", "build:site": "NODE_ENV=production webpack --config webpack.site.js --progress", "postbuild:site": "node scripts/check-single-dexie.js && node scripts/check-site-chunks.js", @@ -148,7 +148,8 @@ "check:editor-text-toolbar": "node --test tests/editor-text-toolbar.spec.mjs", "check:widget-tokens": "node --test tests/widget-tokens.spec.mjs", "check:portal-home-page": "node --test tests/portal-home-page.spec.mjs", - "check:claim-invitation": "node --test tests/claim-invitation.spec.mjs" + "check:claim-invitation": "node --test tests/claim-invitation.spec.mjs", + "check:site-chrome": "node --test tests/site-chrome.spec.mjs" }, "browserslist": [ "extends @nextcloud/browserslist-config" diff --git a/src/shared/i18n/en.json b/src/shared/i18n/en.json index 630e6c6e5..e4f1fa17b 100644 --- a/src/shared/i18n/en.json +++ b/src/shared/i18n/en.json @@ -389,5 +389,6 @@ "The code is right. You now see what is shared with you.": "The code is right. You now see what is shared with you.", "This code is not right or no longer valid. Check the code, or ask for a new one.": "This code is not right or no longer valid. Check the code, or ask for a new one.", "You cannot accept an invitation with this account. Sign in another way and try again.": "You cannot accept an invitation with this account. Sign in another way and try again.", - "Your account is already linked in another way. Contact the organisation that invited you.": "Your account is already linked in another way. Contact the organisation that invited you." + "Your account is already linked in another way. Contact the organisation that invited you.": "Your account is already linked in another way. Contact the organisation that invited you.", + "Search": "Search" } diff --git a/src/shared/i18n/nl.json b/src/shared/i18n/nl.json index afee9bde5..7f652efb7 100644 --- a/src/shared/i18n/nl.json +++ b/src/shared/i18n/nl.json @@ -389,5 +389,6 @@ "The code is right. You now see what is shared with you.": "De code klopt. U ziet nu wat met u is gedeeld.", "This code is not right or no longer valid. Check the code, or ask for a new one.": "Deze code klopt niet of is niet meer geldig. Controleer de code of vraag een nieuwe aan.", "You cannot accept an invitation with this account. Sign in another way and try again.": "Met dit account kunt u geen uitnodiging aannemen. Log op een andere manier in en probeer het opnieuw.", - "Your account is already linked in another way. Contact the organisation that invited you.": "Uw account is al op een andere manier gekoppeld. Neem contact op met de organisatie die u uitnodigde." + "Your account is already linked in another way. Contact the organisation that invited you.": "Uw account is al op een andere manier gekoppeld. Neem contact op met de organisatie die u uitnodigde.", + "Search": "Zoeken" } diff --git a/src/site/App.vue b/src/site/App.vue index 44f81868a..ebd0453bb 100644 --- a/src/site/App.vue +++ b/src/site/App.vue @@ -78,7 +78,13 @@ :userMenuLabel="t('User menu')" :breadcrumbLabel="t('Breadcrumb')" :logoLabel="t('Logo')" + :searchBox="headerSearch" + :searchLabel="t('Search')" + :accountLabel="site.accountLabel || ''" + :accountHref="hrefForRoute('/mijn')" + :menuLabel="t('Menu')" @navigate="go" + @search="goSearch" @signout="signOut">