feat: article editor header redesign

This commit is contained in:
Florian Sylvain committed 2026-11-03 04:24:44 +01:00
1 parent 58fd753559
commit 680b9a1308
6 files changed
+34 -36

No files matched your search

+1 -1
View File
@@ -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 | | 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 | | 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 | | 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 | | ActionOverflow | UDropdownMenu (desktop), UDrawer (mobile) | Disabled items carry a visible reason |
| StatusBadge | UBadge variant soft | Publication: solid icon plus word | | StatusBadge | UBadge variant soft | Publication: solid icon plus word |
| FreshnessBadge | UBadge variant outline | Refresh icon plus word; icon-only at 1024 with aria-label | | FreshnessBadge | UBadge variant outline | Refresh icon plus word; icon-only at 1024 with aria-label |
+5 -5
View File
@@ -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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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" | | 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." | | 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 | | 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" | | 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 | | 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." | | 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) | | 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." | | 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 | | 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 | | 2 | Language dropdown | Visible | Visible | Visible, 44 px |
| 3 | Publication badge | Visible | Visible | Visible | | 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 | | 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 | | State | Look | Message | Save |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Pristine | Heading-size text, neutral border | None | Off | | 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 | | Editing | Primary border, counter n / 200 | Same note | On |
| Unsaved | Neutral border, Unsaved changes badge | Same note | On | | Unsaved | Neutral border, Unsaved changes badge | Same note | On |
| Invalid | Error border and icon | "Enter a title" | Blocked | | Invalid | Error border and icon | "Enter a title" | Blocked |
+6 -5
View File
@@ -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 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 TitleIn = (L, o) => {
const st = o.tst || 'pristine', cap = o.cap, lab = 'Title (' + L.n + ')' 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 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 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] 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), 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 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' : ''), 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), 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)), 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 }))) 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) => { 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 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 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 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)) 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) => { const MBar = (L, o) => {
+5 -5
View File
@@ -2096,7 +2096,7 @@ window.LOC = {
], ],
[ [
"Header priority", "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", "The primary action is never hidden",
"Editor header", "Editor header",
"None", "None",
@@ -2426,7 +2426,7 @@ window.LOC = {
], ],
[ [
"Title input", "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", "Stays in the field; on Try again, back to the field",
"Polite: \"Saving title\", \"Title saved\"; errors are announced" "Polite: \"Saving title\", \"Title saved\"; errors are announced"
], ],
@@ -2869,7 +2869,7 @@ window.LOC = {
"rows": [ "rows": [
[ [
"Title editing", "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", "Slug",
@@ -2970,7 +2970,7 @@ window.LOC = {
"rows": [ "rows": [
[ [
"1", "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", "Full width",
"Full width" "Full width"
@@ -3169,7 +3169,7 @@ window.LOC = {
], ],
[ [
"Focus", "Focus",
"Primary outline", "Primary 2px underline",
"\"Changing the title doesn't change the address.\"", "\"Changing the title doesn't change the address.\"",
"Off" "Off"
], ],
+8 -10
View File
@@ -5,11 +5,9 @@ defineEmits<{ back: [] }>()
</script> </script>
<template> <template>
<div <div class="@container shrink-0 border-b border-default bg-default px-4 py-3 lg:px-6 lg:py-3.5">
class="flex shrink-0 flex-col gap-2.5 border-b border-default bg-default px-4 py-3 lg:px-6 lg:py-3.5" <div class="flex flex-col gap-2.5 lg:flex-row lg:flex-wrap lg:items-start lg:gap-3">
> <span class="hidden h-10 items-center lg:flex">
<div class="flex items-start gap-2">
<span class="hidden h-15 items-end lg:flex">
<UButton <UButton
icon="i-lucide-arrow-left" icon="i-lucide-arrow-left"
color="neutral" color="neutral"
@@ -19,15 +17,15 @@ defineEmits<{ back: [] }>()
@click="$emit('back')" @click="$emit('back')"
/> />
</span> </span>
<div class="min-w-0 flex-1"> <div
class="min-w-0 max-lg:order-first lg:order-3 lg:basis-full @4xl:order-1 @4xl:flex-1 @4xl:basis-60"
>
<slot name="title" /> <slot name="title" />
</div> </div>
</div> <div class="flex min-w-0 flex-wrap items-center gap-2 lg:order-0 lg:min-h-10">
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<slot name="meta" /> <slot name="meta" />
</div> </div>
<div class="hidden shrink-0 lg:flex"> <div class="hidden shrink-0 items-center lg:order-2 lg:ml-auto lg:flex lg:h-10">
<slot name="actions" /> <slot name="actions" />
</div> </div>
</div> </div>
+9 -10
View File
@@ -67,12 +67,14 @@ const trailingIcon = computed(() => {
const borderClass = computed(() => { const borderClass = computed(() => {
if (props.readonly) { if (props.readonly) {
return 'border-0 bg-transparent px-0 py-0 pe-5 lg:min-h-9' return 'bg-transparent px-0 py-0 pe-5 lg:min-h-9'
} }
if (errored.value) { if (errored.value) {
return 'border-error' return 'border-b-2 border-error'
} }
return focused.value ? 'border-primary' : 'border-accented' return focused.value
? 'border-b-2 border-primary bg-default'
: 'border-b border-accented bg-transparent'
}) })
async function remeasure() { async function remeasure() {
@@ -91,7 +93,7 @@ defineExpose({ focus: focusField })
<template> <template>
<div class="flex min-w-0 flex-col" :class="readonly ? 'gap-0.5' : 'gap-1'"> <div class="flex min-w-0 flex-col" :class="readonly ? 'gap-0.5' : 'gap-1'">
<label :for="inputId" class="text-xs/5 text-muted">{{ label }}</label> <label :for="inputId" class="sr-only">{{ label }}</label>
<UTextarea <UTextarea
:id="inputId" :id="inputId"
ref="input" ref="input"
@@ -108,14 +110,11 @@ defineExpose({ focus: focusField })
:ui="{ :ui="{
root: 'w-full', root: 'w-full',
base: [ base: [
'min-h-11 resize-none border bg-default px-2.5 py-0.5 !text-lead/7 font-semibold lg:!text-xl/7 text-highlighted ring-0 lg:min-h-10 lg:py-1.25', 'min-h-11 resize-none rounded-none border-0 px-1 py-0.5 !text-lead/7 font-semibold lg:!text-xl/7 text-highlighted ring-0 lg:min-h-10 lg:py-1.25',
borderClass, borderClass,
showsCounter ? COUNTER_END_PADDING : '', showsCounter ? COUNTER_END_PADDING : '',
focused && !errored && !changed
? 'outline-2 outline-offset-1 outline-primary'
: '',
], ],
trailing: ['items-start gap-2 pt-2.5', readonly ? 'pe-0' : 'pe-2.5'], trailing: ['items-start gap-2 pt-2.5', readonly ? 'pe-0' : 'pe-1'],
trailingIcon: [ trailingIcon: [
'size-3.5', 'size-3.5',
saving ? 'animate-spin' : '', saving ? 'animate-spin' : '',
@@ -139,7 +138,7 @@ defineExpose({ focus: focusField })
v-if="!readonly" v-if="!readonly"
:id="helpId" :id="helpId"
:role="invalid ? 'alert' : undefined" :role="invalid ? 'alert' : undefined"
class="flex flex-wrap items-center gap-2 text-xs/4.5" class="flex flex-wrap items-center gap-2 px-1 text-xs/4.5"
:class="errored ? 'text-error' : 'text-muted'" :class="errored ? 'text-error' : 'text-muted'"
> >
<UIcon v-if="errored" name="i-lucide-circle-x" class="size-3.5" /> <UIcon v-if="errored" name="i-lucide-circle-x" class="size-3.5" />