feat: article editor header redesign
This commit is contained in:
1 parent
58fd753559
commit
680b9a1308
6 files changed
+34
-36
No files matched your search
@@ -175,7 +175,7 @@ Specs: field heights 40 (44 mobile), buttons 36 (44 mobile), card padding 16, se
|
||||
| LanguageMenu | UDropdownMenu with a UButton trigger | Menu button with menu; rows carry badges and a Read only tag; no Add translation row without create permission |
|
||||
| CapabilityGate | none (logic) | Renders each action from capabilities[]; never from the role name |
|
||||
| EditorHeader | UButton, UBadge, UDropdownMenu | Title row, then language, badges and actions. At most one filled button. Collapse order and mobile bottom bar in the header priority table |
|
||||
| TitleInput | UInput (heading size) | Label "Title (French)"; bounds 3 to 200; counter; helper under the field |
|
||||
| TitleInput | UInput (heading size) | Accessible name "Title (French)", no visible label, underline style; bounds 3 to 200; counter; helper under the field |
|
||||
| ActionOverflow | UDropdownMenu (desktop), UDrawer (mobile) | Disabled items carry a visible reason |
|
||||
| StatusBadge | UBadge variant soft | Publication: solid icon plus word |
|
||||
| FreshnessBadge | UBadge variant outline | Refresh icon plus word; icon-only at 1024 with aria-label |
|
||||
|
||||
@@ -36,7 +36,7 @@ Generated from `source/locale-spec.js`. Read `localization.md` first: its explic
|
||||
| Missing vs hidden | "Not in French" lists stories where French is absent or unreadable to the user. Add French translation on a story with an unreadable French returns a neutral 409 (see Visibility policy) | Discovery cannot leak existence | List | Conflict check ignores read permission; response is neutral | Initial |
|
||||
| Source-copy access | Read access to the chosen source is rechecked at submit, inside the transaction | Access can change while the dialog is open | Add translation | Neutral 403/404 | Initial |
|
||||
| Disabled languages | Versions stay readable and editable; publishing blocked until re-enabled; filter lists them under Disabled | No silent unpublishing | Site languages | enabled flag per site language | Initial |
|
||||
| Header priority | Left to right: title input (own row), language dropdown, publication badge, status badge (unsaved, saving, saved, failed), freshness badge, Read only tag; then reference button, Unpublish, Save, primary, overflow. At most one filled button. Collapse order: reference label, Unpublish to overflow, freshness to icon | The primary action is never hidden | Editor header | None | Initial |
|
||||
| Header priority | Left to right: language dropdown, publication badge, status badge (unsaved, saving, saved, failed), freshness badge, Read only tag, title input (fills the remaining width, same row on desktop), then reference button, Unpublish, Save, primary, overflow. At most one filled button. Collapse order: reference label, Unpublish to overflow, freshness to icon | The primary action is never hidden | Editor header | None | Initial |
|
||||
| Public URL pattern | Per-site owner setting with {lang} and {slug}; live preview from a real article; unset state links from "Public URL not configured" | No fabricated website URLs | Settings → Sites | public_url_pattern on site | Initial |
|
||||
| Social locale per language | Per-language owner field on Languages: Derived (from the tag), Set (typed), Not set. Format language_REGION | Never guess a region | Languages page | social_locale per site language | Initial |
|
||||
| Release split | Source reference (L6) and Freshness and review (L9) are release 2. Frames are drawn in full so they can be checked | Smaller first release | None | None | Release 2 |
|
||||
@@ -96,7 +96,7 @@ Generated from `source/locale-spec.js`. Read `localization.md` first: its explic
|
||||
| Control | Accessible name and keyboard | Focus | Announcement |
|
||||
| --- | --- | --- | --- |
|
||||
| Language control | "Language: French. Switch version". Enter or Down opens a menu; arrows move; Enter selects; Escape closes | Into the list on open; back to the control on close | Polite: "Editing French version" |
|
||||
| Title input | Label "Title (French)"; aria-describedby points to the help or error text; invalid sets aria-invalid and the message is an alert | Stays in the field; on Try again, back to the field | Polite: "Saving title", "Title saved"; errors are announced |
|
||||
| Title input | Accessible name "Title (French)", no visible label (the language dropdown beside it names the version); aria-describedby points to the help or error text; invalid sets aria-invalid and the message is an alert | Stays in the field; on Try again, back to the field | Polite: "Saving title", "Title saved"; errors are announced |
|
||||
| Save, Save and publish, Retry publish | Buttons named by their text including the language ("Save and publish French"); busy state sets aria-busy; Tab order: title, language, status, reference, Unpublish, Save, primary, More actions | After save, stays on the button; after a failed save, on Try saving again | Polite: "French saved"; "Saved as a draft. Publishing failed. Try publishing again." |
|
||||
| More actions menu or sheet | Menu button "More actions"; items are menuitems; disabled items use aria-disabled and show the reason as text | First enabled item on open; back to More actions on close | None |
|
||||
| Reference button | "Show source reference" or "Hide source reference", icon-only at 1024 with the same name | Moves into the pane on show; back to the button on hide | Polite: "Showing English reference" |
|
||||
@@ -209,7 +209,7 @@ Generated from `source/locale-spec.js`. Read `localization.md` first: its explic
|
||||
|
||||
| Case | Behavior |
|
||||
| --- | --- |
|
||||
| Title editing | Inline input in heading style. States: pristine, focus, editing, unsaved, invalid ("Enter a title"), too short, too long, saving, failed (value kept). Bounds 3 to 200 characters. Accessible label "Title (French)". Editing the title never changes the slug |
|
||||
| Title editing | Inline underline input in heading style, on the same row as the language, badges and actions on desktop (no box, no visible label). States: pristine, focus, editing, unsaved, invalid ("Enter a title"), too short, too long, saving, failed (value kept). Bounds 3 to 200 characters. Accessible label "Title (French)". Editing the title never changes the slug |
|
||||
| Slug | See Slug policy. Edit slug lives in Search and sharing and in the overflow menu; its note says "Editing the title never changes the slug." |
|
||||
| Conflict | "Someone else saved this version at <time>." Choices: Keep my changes (filled; nothing sent), Download my version, Reload theirs (confirm: "This discards your unsaved changes."), Save over theirs (sends the version fetched now; a newer save gives a new conflict) |
|
||||
| Overwrite scope | "Replaces the title, body and search fields of this version only. Other languages are untouched." |
|
||||
@@ -241,7 +241,7 @@ Generated from `source/locale-spec.js`. Read `localization.md` first: its explic
|
||||
|
||||
| Order | Item | 1440 | 1024 | 390 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Title input (own row, heading style) | Full width | Full width | Full width |
|
||||
| 1 | Title input (underline, heading style; own row on mobile, shares the header row on desktop) | Full width | Full width | Full width |
|
||||
| 2 | Language dropdown | Visible | Visible | Visible, 44 px |
|
||||
| 3 | Publication badge | Visible | Visible | Visible |
|
||||
| 4 | Primary action (the only filled button) | Rightmost before More actions | Same | Fixed bottom bar, fills the width left by Save and More |
|
||||
@@ -276,7 +276,7 @@ Generated from `source/locale-spec.js`. Read `localization.md` first: its explic
|
||||
| State | Look | Message | Save |
|
||||
| --- | --- | --- | --- |
|
||||
| Pristine | Heading-size text, neutral border | None | Off |
|
||||
| Focus | Primary outline | "Changing the title doesn't change the address." | Off |
|
||||
| Focus | Primary 2px underline | "Changing the title doesn't change the address." | Off |
|
||||
| Editing | Primary border, counter n / 200 | Same note | On |
|
||||
| Unsaved | Neutral border, Unsaved changes badge | Same note | On |
|
||||
| Invalid | Error border and icon | "Enter a title" | Blocked |
|
||||
|
||||
@@ -84,15 +84,15 @@ class Component extends DCLogic {
|
||||
const LONGT = 'Comment nous chiffrons une rénovation de salle de bain, poste par poste, avec les coûts cachés derrière les carreaux, sous la baignoire et autour des canalisations anciennes, expliqués avant la commande du premier carreau et de la première vanne'
|
||||
const TitleIn = (L, o) => {
|
||||
const st = o.tst || 'pristine', cap = o.cap, lab = 'Title (' + L.n + ')'
|
||||
if (!cap.e) return D('display:flex;flex-direction:column;gap:2px', S('font-size:12px;' + MU, lab), D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 36) + 'px', A('div', { dir: L.dir, lang: L.lang, role: 'textbox', 'aria-readonly': 'true', 'aria-label': lab, tabIndex: 0 }, 'flex:1;min-width:0;font:600 ' + (m ? 18 : 20) + 'px/28px \'IBM Plex Sans\',sans-serif;' + HL, L.t), Ic('lock', 14)))
|
||||
if (!cap.e) return D('display:flex;flex-direction:column;gap:2px', D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 36) + 'px', A('div', { dir: L.dir, lang: L.lang, role: 'textbox', 'aria-readonly': 'true', 'aria-label': lab, tabIndex: 0 }, 'flex:1;min-width:0;font:600 ' + (m ? 18 : 20) + 'px/28px \'IBM Plex Sans\',sans-serif;' + HL, L.t), Ic('lock', 14)))
|
||||
const val = { invalid: '', short: 'Sa', long: LONGT.slice(0, 203), edit: L.t + ' en 2026', unsaved: L.t + ' en 2026' }[st] ?? (o.titleVal ?? L.t)
|
||||
const err = ['invalid', 'short', 'long', 'failed'].includes(st), on = ['focus', 'edit'].includes(st), cnt = ['focus', 'edit', 'short', 'long'].includes(st)
|
||||
const msg = { invalid: 'Enter a title', short: 'Use at least 3 characters', long: 'Use 200 characters or fewer' }[st]
|
||||
return D('display:flex;flex-direction:column;gap:4px', S('font-size:12px;' + MU, lab),
|
||||
D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 40) + 'px;padding:2px 10px;border:1px solid ' + (err ? 'var(--ui-error)' : on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:var(--ui-bg);' + (st === 'focus' ? 'outline:2px solid var(--ui-primary);outline-offset:1px' : ''),
|
||||
return D('display:flex;flex-direction:column;gap:2px',
|
||||
D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 40) + 'px;padding:2px 4px;border:0;border-bottom:' + (on || err ? 2 : 1) + 'px solid ' + (err ? 'var(--ui-error)' : on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on ? 'var(--ui-bg)' : 'transparent') + ';' + (st === 'focus' ? 'box-shadow:0 2px 0 0 var(--ui-primary)' : ''),
|
||||
A('div', { dir: L.dir, lang: L.lang, role: 'textbox', 'aria-label': lab, 'aria-invalid': err || undefined, 'aria-describedby': 'title-help', tabIndex: 0 }, 'flex:1;min-width:0;font:600 ' + (m ? 18 : 20) + 'px/28px \'IBM Plex Sans\',sans-serif;' + (st === 'invalid' ? MU : HL) + ';text-wrap:pretty;' + (m ? '' : 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap'), st === 'invalid' ? 'Title' : val),
|
||||
cnt && S(mono + ';flex-shrink:0;color:' + (err ? 'var(--ui-error)' : 'var(--ui-text-muted)'), val.length + ' / 200'), st === 'saving' ? Ic('loader-circle', 14) : st === 'failed' ? Ic('circle-x', 14) : Ic('pencil', 14)),
|
||||
A('div', { id: 'title-help', role: err ? 'alert' : undefined }, 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;font-size:12px;line-height:18px;' + (err ? 'color:var(--ui-error)' : MU),
|
||||
A('div', { id: 'title-help', role: err ? 'alert' : undefined }, 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:0 4px;font-size:12px;line-height:18px;' + (err ? 'color:var(--ui-error)' : MU),
|
||||
err && Ic('circle-x', 14), msg || (st === 'failed' ? 'Couldn\'t save the title. Your text is kept.' : st === 'saving' ? 'Saving title…' : (on || st === 'unsaved') ? 'Changing the title doesn\'t change the address.' : null), st === 'failed' && Btn('Try again', 'secondary', { sm: true })))
|
||||
}
|
||||
const Acts = (L, o) => {
|
||||
@@ -128,7 +128,8 @@ class Component extends DCLogic {
|
||||
const status = o.saving ? Bd('Saving…', 'neutral', 'loader-circle') : o.saveFail ? Bd('Save failed', 'error', 'circle-x') : o.pubFail || o.saved ? Bd('Saved', 'success', 'check') : (o.dirty || o.tst === 'unsaved') ? Bd('Unsaved changes', 'neutral', 'circle-dot') : null
|
||||
const fresh = o.fresh && (nr ? A('span', { 'aria-label': o.fresh, title: o.fresh }, 'display:inline-flex;align-items:center;justify-content:center;width:24px;height:20px;flex-shrink:0;border:1px solid currentColor;background:var(--ui-bg);color:var(--ui-warning)', Ic('refresh-cw', 12)) : Fr(o.fresh))
|
||||
const left = D('flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:8px;flex-wrap:wrap', LangBtn(L.n, { code: L.c, open: !!o.open }), Pub(o.pub), status, fresh, !cap.e && Bd('Read only', 'neutral', 'lock'))
|
||||
const right = !m && D('display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:auto', Btn(nr ? '' : refL, 'secondary', { ic: 'columns-2', al: refL }), a.un && !nr && Btn(a.un.l), ...a.sec.map(x2 => Btn(x2.l, 'secondary', { dis: x2.dis })), pb(a.prim), items.length ? Btn('', 'secondary', { ic: 'ellipsis', al: 'More actions' }) : null)
|
||||
const right = !m && D('display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:auto;height:40px', Btn(nr ? '' : refL, 'secondary', { ic: 'columns-2', al: refL }), a.un && !nr && Btn(a.un.l), ...a.sec.map(x2 => Btn(x2.l, 'secondary', { dis: x2.dis })), pb(a.prim), items.length ? Btn('', 'secondary', { ic: 'ellipsis', al: 'More actions' }) : null)
|
||||
if (!m) return D('flex:1 1 100%;min-width:0;display:flex;align-items:flex-start;gap:12px;flex-wrap:' + (nr ? 'wrap' : 'nowrap'), D('height:40px;display:flex;align-items:center;' + MU, Ic('arrow-left', 16)), D('display:flex;align-items:center;gap:8px;height:40px;flex-shrink:0;flex-wrap:nowrap', ...[LangBtn(L.n, { code: L.c, open: !!o.open }), Pub(o.pub), status, fresh, !cap.e && Bd('Read only', 'neutral', 'lock')].filter(Boolean)), D('flex:1 1 ' + (nr ? '100%' : '240px') + ';min-width:0;order:' + (nr ? 3 : 0), TitleIn(L, o)), right)
|
||||
return D('flex:1 1 100%;min-width:0;display:flex;flex-direction:column;gap:10px', D('display:flex;align-items:flex-start;gap:8px', !m && D('height:60px;display:flex;align-items:flex-end;' + MU, Ic('arrow-left', 16)), D('flex:1;min-width:0', TitleIn(L, o))), D('display:flex;align-items:center;gap:12px;flex-wrap:wrap', left, right))
|
||||
}
|
||||
const MBar = (L, o) => {
|
||||
|
||||
@@ -2096,7 +2096,7 @@ window.LOC = {
|
||||
],
|
||||
[
|
||||
"Header priority",
|
||||
"Left to right: title input (own row), language dropdown, publication badge, status badge (unsaved, saving, saved, failed), freshness badge, Read only tag; then reference button, Unpublish, Save, primary, overflow. At most one filled button. Collapse order: reference label, Unpublish to overflow, freshness to icon",
|
||||
"Left to right: language dropdown, publication badge, status badge (unsaved, saving, saved, failed), freshness badge, Read only tag, title input (fills the remaining width, same row on desktop), then reference button, Unpublish, Save, primary, overflow. At most one filled button. Collapse order: reference label, Unpublish to overflow, freshness to icon",
|
||||
"The primary action is never hidden",
|
||||
"Editor header",
|
||||
"None",
|
||||
@@ -2426,7 +2426,7 @@ window.LOC = {
|
||||
],
|
||||
[
|
||||
"Title input",
|
||||
"Label \"Title (French)\"; aria-describedby points to the help or error text; invalid sets aria-invalid and the message is an alert",
|
||||
"Accessible name \"Title (French)\", no visible label (the language dropdown beside it names the version); aria-describedby points to the help or error text; invalid sets aria-invalid and the message is an alert",
|
||||
"Stays in the field; on Try again, back to the field",
|
||||
"Polite: \"Saving title\", \"Title saved\"; errors are announced"
|
||||
],
|
||||
@@ -2869,7 +2869,7 @@ window.LOC = {
|
||||
"rows": [
|
||||
[
|
||||
"Title editing",
|
||||
"Inline input in heading style. States: pristine, focus, editing, unsaved, invalid (\"Enter a title\"), too short, too long, saving, failed (value kept). Bounds 3 to 200 characters. Accessible label \"Title (French)\". Editing the title never changes the slug"
|
||||
"Inline underline input in heading style, on the same row as the language, badges and actions on desktop (no box, no visible label). States: pristine, focus, editing, unsaved, invalid (\"Enter a title\"), too short, too long, saving, failed (value kept). Bounds 3 to 200 characters. Accessible label \"Title (French)\". Editing the title never changes the slug"
|
||||
],
|
||||
[
|
||||
"Slug",
|
||||
@@ -2970,7 +2970,7 @@ window.LOC = {
|
||||
"rows": [
|
||||
[
|
||||
"1",
|
||||
"Title input (own row, heading style)",
|
||||
"Title input (underline, heading style; own row on mobile, shares the header row on desktop)",
|
||||
"Full width",
|
||||
"Full width",
|
||||
"Full width"
|
||||
@@ -3169,7 +3169,7 @@ window.LOC = {
|
||||
],
|
||||
[
|
||||
"Focus",
|
||||
"Primary outline",
|
||||
"Primary 2px underline",
|
||||
"\"Changing the title doesn't change the address.\"",
|
||||
"Off"
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user