diff --git a/backend/api/authentication.ts b/backend/api/authentication.ts index dc4a601ea..a862b7e07 100644 --- a/backend/api/authentication.ts +++ b/backend/api/authentication.ts @@ -100,6 +100,11 @@ async function routes(app: FastifyInstance) { registration: { type: 'boolean' }, + allowForgotPassword: { + type: 'boolean', + description: + 'Whether this strategy offers a password reset from the login screen. False for a strategy whose module has no such setting.' + }, strategy: { type: 'object', properties: { @@ -136,7 +141,14 @@ async function routes(app: FastifyInstance) { if (!site) { return reply.badRequest('Invalid Site ID') } - const activeStrategies = await WIKI.models.authentication.getStrategies({ enabledOnly: true }) + /* + `getActiveStrategies` rather than the raw rows: it completes each config from the module's + declared defaults, so a prop added to a module after a strategy was configured reads as its + default here instead of as a missing key. + */ + const activeStrategies = (await WIKI.models.authentication.getActiveStrategies()).filter( + (str: any) => str.isEnabled + ) // -> A site created before it had strategies configured has no list at all const configuredStrategies = site.config.authStrategies ?? [] const siteStrategies = activeStrategies @@ -150,6 +162,14 @@ async function routes(app: FastifyInstance) { activeStrategy: { displayName: str.displayName, registration: str.registration, + /* + Named explicitly, like every other field here: this endpoint is public and a strategy's + config is where an OAuth client secret lives, so nothing may reach it by spreading. + + A module that declares no such prop reads as false, which is correct rather than a + default -- a strategy with no password of its own has no password to reset. + */ + allowForgotPassword: str.config?.allowForgotPassword === true, strategy: { key: authModule?.key ?? str.module, title: authModule?.title ?? str.module, diff --git a/backend/locales/en.json b/backend/locales/en.json index 6c938a7e7..b0f779732 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -1737,10 +1737,10 @@ "editor.emoji.smileysEmotion": "Smileys & Emotion", "editor.emoji.symbols": "Symbols", "editor.emoji.travelPlaces": "Travel & Places", - "editor.markup.admonitionDanger": "Danger / Caution Admonition", + "editor.markup.admonitionDanger": "Caution Admonition", "editor.markup.admonitionImportant": "Important Admonition", - "editor.markup.admonitionInfo": "Info / Note Admonition", - "editor.markup.admonitionSuccess": "Tip / Success Admonition", + "editor.markup.admonitionInfo": "Note Admonition", + "editor.markup.admonitionSuccess": "Tip Admonition", "editor.markup.admonitionWarning": "Warning Admonition", "editor.markup.blockquote": "Blockquote", "editor.markup.blockquoteAdmonitions": "Blockquote / Admonition", @@ -1750,6 +1750,7 @@ "editor.markup.blockquoteWarning": "Warning Blockquote", "editor.markup.bold": "Bold", "editor.markup.distractionFreeMode": "Distraction Free Mode", + "editor.markup.editTable": "Edit in Table Editor", "editor.markup.header": "Header", "editor.markup.headerLevel": "Header {level}", "editor.markup.heading": "Heading {level}", @@ -1763,7 +1764,6 @@ "editor.markup.insertHorizontalBar": "Insert Horizontal Bar", "editor.markup.insertLink": "Insert Link", "editor.markup.insertMathExpression": "Insert Math Expression", - "editor.markup.editTable": "Edit in Table Editor", "editor.markup.insertTable": "Insert Table", "editor.markup.insertTabset": "Insert Tabset", "editor.markup.insertVideoAudio": "Insert Video / Audio", diff --git a/backend/modules/authentication/local/definition.yml b/backend/modules/authentication/local/definition.yml index 85ff0ebd8..bc92a2245 100644 --- a/backend/modules/authentication/local/definition.yml +++ b/backend/modules/authentication/local/definition.yml @@ -23,3 +23,9 @@ props: hint: Send a verification email to the user with a validation link when registering (if registration is enabled). icon: received default: true + allowForgotPassword: + type: Boolean + title: Allow Forgot Password + hint: Users who have forgotten their password can request a reset link by email from the login screen. Turn this off where passwords are handed out rather than chosen. + icon: password-reset + default: true diff --git a/frontend/public/_assets/icons/fluent-access.svg b/frontend/public/_assets/icons/fluent-access.svg new file mode 100644 index 000000000..d15d3dd64 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-access.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-history.svg b/frontend/public/_assets/icons/fluent-history.svg new file mode 100644 index 000000000..71a044764 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-home.svg b/frontend/public/_assets/icons/fluent-home.svg new file mode 100644 index 000000000..407c9df7e --- /dev/null +++ b/frontend/public/_assets/icons/fluent-home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-live.svg b/frontend/public/_assets/icons/fluent-live.svg new file mode 100644 index 000000000..8d77f12b8 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-live.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-source-code.svg b/frontend/public/_assets/icons/fluent-source-code.svg new file mode 100644 index 000000000..85a9ccc21 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-source-code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-task.svg b/frontend/public/_assets/icons/fluent-task.svg new file mode 100644 index 000000000..00007f39b --- /dev/null +++ b/frontend/public/_assets/icons/fluent-task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-unfold.svg b/frontend/public/_assets/icons/fluent-unfold.svg new file mode 100644 index 000000000..2414fc02a --- /dev/null +++ b/frontend/public/_assets/icons/fluent-unfold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-web-design.svg b/frontend/public/_assets/icons/fluent-web-design.svg new file mode 100644 index 000000000..6e84497a6 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-web-design.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/AuthLoginPanel.vue b/frontend/src/components/AuthLoginPanel.vue index fa4bf0bdf..6b38fda9a 100644 --- a/frontend/src/components/AuthLoginPanel.vue +++ b/frontend/src/components/AuthLoginPanel.vue @@ -112,8 +112,11 @@ no-caps icon="la:user-plus" @click="switchTo(`register`)" /> + - + {{ t('editor.markup.blockquoteAdmonitions') }} diff --git a/frontend/src/components/PageHeader.vue b/frontend/src/components/PageHeader.vue index f95f41d5d..949cf9cf8 100644 --- a/frontend/src/components/PageHeader.vue +++ b/frontend/src/components/PageHeader.vue @@ -13,7 +13,11 @@ style="min-height: 64px"> - + + + + @@ -451,6 +455,19 @@ async function seedEditables() { syncEditable(descriptionEl.value, pageStore.description) } +/** + * The icon picked from the header. + * + * The same two steps the title and the description take below, and for the same reason: an icon is part + * of the page, so changing it has to leave the editor holding an unsaved change. Bound through the event + * rather than `v-model` because that wrote the store and nothing else -- Save Changes stayed disabled + * until something else was edited, and closing the editor threw the new icon away without a word. + */ +function setIcon(icon) { + pageStore.icon = icon + editorStore.lastChangeTimestamp = Temporal.Now.instant() +} + function onEditableInput(field, event) { // -> Clearing the field leaves a browser-inserted `
` behind, which contributes nothing to the // text but does hold a second line open under the placeholder diff --git a/frontend/src/css/_page-contents.scss b/frontend/src/css/_page-contents.scss index b092d6fec..f439aa023 100644 --- a/frontend/src/css/_page-contents.scss +++ b/frontend/src/css/_page-contents.scss @@ -765,7 +765,20 @@ &::before { content: ''; position: absolute; - top: 1em; + /* + Centred on the CAP BAND of the first line, not on the line box that holds it. + + A line box is ascent plus descent, and a title like "Note" or "Caution" inks only the part + between the cap line and the baseline -- so an icon centred on the line came out a pixel high, + and at this size that reads as its bottom edge running flush with the baseline while its top + towers over the capitals. Which is why the warning triangle looked right and nothing else did: + its mass is all at the base, so a bottom-flush box still reads as balanced. + + `0.9em` is the padding above the first line, `0.79em` the middle of the cap band within it + (half-leading plus ascent, less half the cap height, for the app's Roboto at 1.6 line-height), + and `0.625em` half the icon. + */ + top: calc(0.9em + 0.79em - 0.625em); left: 1.1em; width: 1.25em; height: 1.25em; diff --git a/frontend/src/renderers/modules/github-alerts.js b/frontend/src/renderers/modules/github-alerts.js index 7c14cbe3d..447182e98 100644 --- a/frontend/src/renderers/modules/github-alerts.js +++ b/frontend/src/renderers/modules/github-alerts.js @@ -22,26 +22,32 @@ const KINDS = new Map([ ]) /** - * The marker, which has to be the whole of the blockquote's first line. + * The marker, and whatever the author wrote after it on the same line. * - * Anything after it on that line means the author wrote a blockquote that happens to open with - * brackets, which is what GitHub decides too — and the line is then left exactly as it was typed. + * That remainder is the admonition's title: `> [!NOTE] Read this first` is headed "Read this first" + * rather than "Note". A deliberate step past GitHub, which renders those words as the first line of the + * quote instead — and the step every other implementation of these takes, because a marker's own label + * says only what kind of aside it is, never what this one is about. + * + * Left off, the kind's own label stands in, exactly as before. What is captured is raw markdown and is + * parsed as such (see `titleTokens`), so a title may hold a link or a `code` span like any other line. */ -const MARKER = /^\[!([a-z]+)\][ \t]*(?:\n|$)/i +const MARKER = /^\[!([a-z]+)\][ \t]*([^\n]*)(?:\n|$)/i /** - * The label, as three tokens: a paragraph carrying a class, its inline content, and the close. + * The title, as three tokens: a paragraph carrying a class, its inline content, and the close. * * The inline token is left with nothing but `content`; the core `inline` rule runs after this one and - * is what turns that into children, the same as for every other paragraph on the page. + * is what turns that into children, the same as for every other paragraph on the page — which is also + * what lets an author's own title carry markdown, and what keeps it escaped if it carries anything else. */ -function labelTokens(state, label) { +function titleTokens(state, title) { const open = new state.Token('paragraph_open', 'p', 1) open.attrSet('class', 'alert-title') open.block = true const inline = new state.Token('inline', '', 0) - inline.content = label + inline.content = title inline.children = [] const close = new state.Token('paragraph_close', 'p', -1) @@ -79,13 +85,16 @@ export default (md) => { // top of that is what the stylesheet is written to expect tokens[i].attrJoin('class', kind.className) + // -> A title of nothing but spaces is no title: the kind says what it is instead + const title = marker[2].trim() || kind.label + const rest = tokens[i + 2].content.slice(marker[0].length) if (rest) { tokens[i + 2].content = rest - tokens.splice(i + 1, 0, ...labelTokens(state, kind.label)) + tokens.splice(i + 1, 0, ...titleTokens(state, title)) } else { - // -> The marker was the whole paragraph, so the label takes its place rather than joining it - tokens.splice(i + 1, 3, ...labelTokens(state, kind.label)) + // -> The marker line was the whole paragraph, so the title takes its place rather than joining it + tokens.splice(i + 1, 3, ...titleTokens(state, title)) } } })