Quellcode durchsuchen

Make the slogan stand-in look like one, and fix the gallery's reach (#281)

* Say when the sheet is standing in for a slogan, and let the gallery scroll

A workshop with no slogan saw one anyway: the canvas fills that block with a
stand-in so it can be found and placed, but it reads as a real line, and the
question it prompts is where the slogan nobody wrote came from. It also
prints as nothing, so the sheet on screen and the sheet on paper disagree in
exactly that spot.

The stand-in now looks like one, with a dashed edge and a marker on the
canvas, and selecting the slogan explains where the real one is set, opening
company settings in a new tab so a layout in progress survives the trip.

Separately, the gallery could not reach its own footer. It asked for a
minimum height inside a frame that does not scroll, so it grew past the
viewport and was clipped rather than scrolling, taking the last row of
designs and the way back to settings with it.

* Put the way out and the way on above the gallery, not below it

Both sat under a screen of template cards. Leaving needed a scroll, which an
escape hatch never should, and carrying on with the layout somebody already
has, which is the commonest reason to open this page at all, was the hardest
thing on it to reach while replacing that layout filled the view.

Back to settings is now a plain link at the top left, matching the back link
the inspector already uses. Continue is a button at the top right, outlined
rather than filled so it is easy to find without shouting over the templates
somebody may genuinely have come to browse.
Bernt Christian Egeland vor 1 Monat
Ursprung
Commit
cd4233a9d4

+ 6 - 1
messages/de/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Schalten Sie das Logo unter Felder ein, um es auch am Seitenfuß zu drucken.",
     "logoReset": "Firmenlogo verwenden",
     "logoOwn": "Wird nur auf diesem Dokument gedruckt. Das Firmenlogo bleibt unverändert.",
-    "logoInherited": "Es wird das Firmenlogo verwendet. Laden Sie hier eines hoch, um nur auf diesem Dokument ein anderes zu verwenden."
+    "logoInherited": "Es wird das Firmenlogo verwendet. Laden Sie hier eines hoch, um nur auf diesem Dokument ein anderes zu verwenden.",
+    "slogan": "Slogan",
+    "sloganPlaceholderHint": "Es ist kein Slogan hinterlegt, daher zeigt das Blatt einen Platzhalter, der nicht gedruckt wird.",
+    "sloganSetHint": "Ihr Slogan stammt aus Ihren Firmendaten.",
+    "sloganLink": "Firmeneinstellungen",
+    "placeholder": "Platzhalter"
   },
   "preview": {
     "title": "Bremsenservice und Ölwechsel",

+ 6 - 1
messages/en/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Switch the logo on under Fields to print it at the foot of the page too.",
     "logoReset": "Use the company logo",
     "logoOwn": "Printed on this document only. The company logo is unchanged.",
-    "logoInherited": "Using the company logo. Upload one here to use a different mark on this document only."
+    "logoInherited": "Using the company logo. Upload one here to use a different mark on this document only.",
+    "slogan": "Slogan",
+    "sloganPlaceholderHint": "No slogan is set, so the sheet shows a stand-in that will not print.",
+    "sloganSetHint": "Your slogan comes from your company details.",
+    "sloganLink": "Company settings",
+    "placeholder": "Placeholder"
   },
   "preview": {
     "title": "Brake Service & Oil Change",

+ 6 - 1
messages/es/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Activa el logotipo en Campos para imprimirlo también al pie de la página.",
     "logoReset": "Usar el logotipo de la empresa",
     "logoOwn": "Se imprime solo en este documento. El logotipo de la empresa no cambia.",
-    "logoInherited": "Se usa el logotipo de la empresa. Sube uno aquí para usar otro solo en este documento."
+    "logoInherited": "Se usa el logotipo de la empresa. Sube uno aquí para usar otro solo en este documento.",
+    "slogan": "Eslogan",
+    "sloganPlaceholderHint": "No hay ningún eslogan definido, así que la hoja muestra un texto de ejemplo que no se imprimirá.",
+    "sloganSetHint": "Tu eslogan proviene de los datos de tu empresa.",
+    "sloganLink": "Ajustes de empresa",
+    "placeholder": "Marcador"
   },
   "preview": {
     "title": "Servicio de frenos y cambio de aceite",

+ 6 - 1
messages/fr/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Activez le logo dans Champs pour l'imprimer aussi en bas de page.",
     "logoReset": "Utiliser le logo de l'entreprise",
     "logoOwn": "Imprimé sur ce document uniquement. Le logo de l'entreprise reste inchangé.",
-    "logoInherited": "Le logo de l'entreprise est utilisé. Importez-en un ici pour en utiliser un autre sur ce document seulement."
+    "logoInherited": "Le logo de l'entreprise est utilisé. Importez-en un ici pour en utiliser un autre sur ce document seulement.",
+    "slogan": "Slogan",
+    "sloganPlaceholderHint": "Aucun slogan n'est défini, la feuille affiche donc un texte d'exemple qui ne sera pas imprimé.",
+    "sloganSetHint": "Votre slogan provient des informations de votre entreprise.",
+    "sloganLink": "Paramètres de l'entreprise",
+    "placeholder": "Exemple"
   },
   "preview": {
     "title": "Révision des freins et vidange",

+ 6 - 1
messages/it/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Attiva il logo in Campi per stamparlo anche a piè di pagina.",
     "logoReset": "Usa il logo aziendale",
     "logoOwn": "Stampato solo su questo documento. Il logo aziendale resta invariato.",
-    "logoInherited": "Viene usato il logo aziendale. Caricane uno qui per usarne un altro solo su questo documento."
+    "logoInherited": "Viene usato il logo aziendale. Caricane uno qui per usarne un altro solo su questo documento.",
+    "slogan": "Slogan",
+    "sloganPlaceholderHint": "Nessuno slogan impostato, quindi il foglio mostra un segnaposto che non verrà stampato.",
+    "sloganSetHint": "Lo slogan proviene dai dati della tua azienda.",
+    "sloganLink": "Impostazioni azienda",
+    "placeholder": "Segnaposto"
   },
   "preview": {
     "title": "Servizio freni e cambio olio",

+ 6 - 1
messages/lt/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Įjunkite logotipą skiltyje Laukai, kad jis būtų spausdinamas ir puslapio apačioje.",
     "logoReset": "Naudoti įmonės logotipą",
     "logoOwn": "Spausdinamas tik šiame dokumente. Įmonės logotipas nekeičiamas.",
-    "logoInherited": "Naudojamas įmonės logotipas. Įkelkite kitą čia, kad jis būtų naudojamas tik šiame dokumente."
+    "logoInherited": "Naudojamas įmonės logotipas. Įkelkite kitą čia, kad jis būtų naudojamas tik šiame dokumente.",
+    "slogan": "Šūkis",
+    "sloganPlaceholderHint": "Šūkis nenustatytas, todėl lape rodomas pavyzdinis tekstas, kuris nebus spausdinamas.",
+    "sloganSetHint": "Jūsų šūkis imamas iš įmonės duomenų.",
+    "sloganLink": "Įmonės nustatymai",
+    "placeholder": "Vietaženklis"
   },
   "preview": {
     "title": "Stabdžių aptarnavimas ir tepalų keitimas",

+ 6 - 1
messages/nb/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Slå på logoen under Felter for å skrive den ut nederst på siden også.",
     "logoReset": "Bruk firmalogoen",
     "logoOwn": "Skrives bare ut på dette dokumentet. Firmalogoen er uendret.",
-    "logoInherited": "Bruker firmalogoen. Last opp en her for å bruke en annen logo bare på dette dokumentet."
+    "logoInherited": "Bruker firmalogoen. Last opp en her for å bruke en annen logo bare på dette dokumentet.",
+    "slogan": "Slagord",
+    "sloganPlaceholderHint": "Det er ikke satt noe slagord, så arket viser en plassholder som ikke skrives ut.",
+    "sloganSetHint": "Slagordet hentes fra firmaopplysningene dine.",
+    "sloganLink": "Firmainnstillinger",
+    "placeholder": "Plassholder"
   },
   "preview": {
     "title": "Bremseservice og oljeskift",

+ 6 - 1
messages/nl/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Zet het logo aan bij Velden om het ook onderaan de pagina af te drukken.",
     "logoReset": "Bedrijfslogo gebruiken",
     "logoOwn": "Wordt alleen op dit document afgedrukt. Het bedrijfslogo blijft ongewijzigd.",
-    "logoInherited": "Het bedrijfslogo wordt gebruikt. Upload er hier een om alleen op dit document een ander te gebruiken."
+    "logoInherited": "Het bedrijfslogo wordt gebruikt. Upload er hier een om alleen op dit document een ander te gebruiken.",
+    "slogan": "Slogan",
+    "sloganPlaceholderHint": "Er is geen slogan ingesteld, dus het blad toont een voorbeeldtekst die niet wordt afgedrukt.",
+    "sloganSetHint": "Je slogan komt uit je bedrijfsgegevens.",
+    "sloganLink": "Bedrijfsinstellingen",
+    "placeholder": "Voorbeeld"
   },
   "preview": {
     "title": "Remservice en oliewissel",

+ 6 - 1
messages/pl/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Włącz logo w sekcji Pola, aby drukować je także na dole strony.",
     "logoReset": "Użyj logo firmy",
     "logoOwn": "Drukowane tylko na tym dokumencie. Logo firmy pozostaje bez zmian.",
-    "logoInherited": "Używane jest logo firmy. Prześlij tutaj inne, aby użyć go tylko na tym dokumencie."
+    "logoInherited": "Używane jest logo firmy. Prześlij tutaj inne, aby użyć go tylko na tym dokumencie.",
+    "slogan": "Slogan",
+    "sloganPlaceholderHint": "Nie ustawiono sloganu, więc arkusz pokazuje tekst zastępczy, który nie zostanie wydrukowany.",
+    "sloganSetHint": "Twój slogan pochodzi z danych firmy.",
+    "sloganLink": "Ustawienia firmy",
+    "placeholder": "Symbol zastępczy"
   },
   "preview": {
     "title": "Serwis hamulców i wymiana oleju",

+ 6 - 1
messages/pt-BR/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Ative o logotipo em Campos para imprimi-lo também no rodapé.",
     "logoReset": "Usar o logotipo da empresa",
     "logoOwn": "Impresso somente neste documento. O logotipo da empresa não muda.",
-    "logoInherited": "Usando o logotipo da empresa. Envie um aqui para usar outro somente neste documento."
+    "logoInherited": "Usando o logotipo da empresa. Envie um aqui para usar outro somente neste documento.",
+    "slogan": "Slogan",
+    "sloganPlaceholderHint": "Nenhum slogan definido, então a folha mostra um texto de exemplo que não será impresso.",
+    "sloganSetHint": "Seu slogan vem dos dados da sua empresa.",
+    "sloganLink": "Configurações da empresa",
+    "placeholder": "Exemplo"
   },
   "preview": {
     "title": "Serviço de freios e troca de óleo",

+ 6 - 1
messages/ru/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Включите логотип в разделе «Поля», чтобы печатать его и внизу страницы.",
     "logoReset": "Использовать логотип компании",
     "logoOwn": "Печатается только в этом документе. Логотип компании не меняется.",
-    "logoInherited": "Используется логотип компании. Загрузите другой, чтобы применить его только к этому документу."
+    "logoInherited": "Используется логотип компании. Загрузите другой, чтобы применить его только к этому документу.",
+    "slogan": "Слоган",
+    "sloganPlaceholderHint": "Слоган не задан, поэтому на листе показан образец, который не будет напечатан.",
+    "sloganSetHint": "Слоган берётся из данных вашей компании.",
+    "sloganLink": "Настройки компании",
+    "placeholder": "Образец"
   },
   "preview": {
     "title": "Обслуживание тормозов и замена масла",

+ 6 - 1
messages/tr/settings.json

@@ -1876,7 +1876,12 @@
     "footerLogoHint": "Sayfanın altında da yazdırmak için Alanlar bölümünden logoyu açın.",
     "logoReset": "Şirket logosunu kullan",
     "logoOwn": "Yalnızca bu belgede yazdırılır. Şirket logosu değişmez.",
-    "logoInherited": "Şirket logosu kullanılıyor. Yalnızca bu belgede farklı bir logo kullanmak için buraya yükleyin."
+    "logoInherited": "Şirket logosu kullanılıyor. Yalnızca bu belgede farklı bir logo kullanmak için buraya yükleyin.",
+    "slogan": "Slogan",
+    "sloganPlaceholderHint": "Slogan tanımlanmadığı için sayfada yazdırılmayacak bir örnek metin gösteriliyor.",
+    "sloganSetHint": "Sloganınız şirket bilgilerinizden gelir.",
+    "sloganLink": "Şirket ayarları",
+    "placeholder": "Örnek"
   },
   "preview": {
     "title": "Fren bakımı ve yağ değişimi",

+ 25 - 0
src/features/invoice-designer/Components/DesignerInspector.tsx

@@ -315,6 +315,7 @@ export function DesignerInspector({
   logoUrl,
   ownLogo,
   onLogo,
+  sloganSet,
 }: {
   layout: InvoiceLayoutConfig
   template: DesignerTemplate
@@ -332,6 +333,8 @@ export function DesignerInspector({
   /** Whether that picture is this document's own rather than the company's. */
   ownLogo: boolean
   onLogo: (url: string) => void
+  /** Whether the workshop has a slogan, or the canvas is showing a stand-in. */
+  sloganSet: boolean
 }) {
   const t = useTranslations('settings.designer')
   const tSection = useTranslations('settings.layoutEditor.sections')
@@ -502,6 +505,28 @@ export function DesignerInspector({
             <p className="text-[11.5px] leading-snug text-[#8a8f97]">{t('spacingHint')}</p>
           </Group>
 
+          {/* The slogan is the workshop's own words, kept in company settings
+              rather than here, because it is the same line wherever it is
+              printed. When there is none the canvas shows a stand-in so the
+              block can be found and placed, and that has to say so: it looks
+              exactly like a real slogan otherwise, and it prints as nothing.
+              Opened in a new tab so a layout in progress is not lost. */}
+          {section.id === 'slogan' && (
+            <Group title={t('slogan')}>
+              <p className="text-[11.5px] leading-snug text-[#8a8f97]">
+                {sloganSet ? t('sloganSetHint') : t('sloganPlaceholderHint')}{' '}
+                <a
+                  href="/settings/company"
+                  target="_blank"
+                  rel="noreferrer"
+                  className="font-medium text-[#2563eb] underline underline-offset-2"
+                >
+                  {t('sloganLink')}
+                </a>
+              </p>
+            </Group>
+          )}
+
           {section.id === 'header' && (
             <Group title={t('logo')}>
               <LogoUpload value={logoUrl} own={ownLogo} onChange={onLogo} />

+ 38 - 18
src/features/invoice-designer/Components/InvoiceDesigner.tsx

@@ -199,6 +199,13 @@ export function InvoiceDesigner({
 
   const layout = layouts[docType]
   const template = templates[docType]
+  // Blocks the canvas is filling in for the workshop. The slogan is the only
+  // one today: the rest of the sample stands in for a job, which a real sheet
+  // will have, while a slogan nobody has written simply never prints.
+  const placeholderIds = useMemo(
+    () => new Set(companyWorkshop.slogan?.trim() ? [] : ['slogan']),
+    [companyWorkshop.slogan]
+  )
   // What this document actually prints: its own mark when it has one, the
   // company logo otherwise. The same fallback the print routes apply, so the
   // canvas cannot promise a picture the paper will not carry.
@@ -622,9 +629,37 @@ export function InvoiceDesigner({
   }
 
   if (view === 'gallery') {
+    // A bounded height, not a minimum: the tool sits in a fixed, non-scrolling
+    // frame, so a gallery that grows past the viewport has to scroll inside
+    // itself. With min-h-screen it simply grew and the frame clipped it,
+    // putting the last row of designs and the way back out of reach.
     return (
-      <div className="flex min-h-screen flex-col items-center overflow-y-auto px-8 py-14">
+      <div className="flex h-full flex-col items-center overflow-y-auto px-8 py-14">
         <div className="w-full max-w-[1060px]">
+          {/* The way out and the way on, both above the cards. Leaving should
+              never need a scroll, and carrying on with the layout somebody
+              already has is the commonest reason to open this page: below a
+              screen of templates it was the hardest thing here to reach,
+              while replacing that layout filled the view. */}
+          <div className="mb-7 flex items-center justify-between gap-4">
+            <button
+              type="button"
+              onClick={() => router.push('/settings/templates')}
+              className="text-sm text-[#71767e] hover:text-[#1a1d21]"
+            >
+              ◂ {t('backToSettings')}
+            </button>
+            {/* Outlined rather than filled: it must be easy to find without
+                shouting over the templates somebody may have come to browse. */}
+            <button
+              type="button"
+              onClick={() => setView('designer')}
+              className="rounded-[7px] border border-[#c9ccd1] bg-white px-3 py-1.5 text-sm font-medium text-[#2563eb] hover:border-[#2563eb]"
+            >
+              {t('continueCurrent')} →
+            </button>
+          </div>
+
           <div className="text-[13px] font-semibold uppercase tracking-[0.08em] text-[#2563eb]">
             {t('eyebrow')}
           </div>
@@ -710,23 +745,6 @@ export function InvoiceDesigner({
               )
             })}
           </div>
-
-          <div className="mt-7 flex justify-center gap-6">
-            <button
-              type="button"
-              onClick={() => setView('designer')}
-              className="text-sm font-medium text-[#2563eb]"
-            >
-              {t('continueCurrent')} →
-            </button>
-            <button
-              type="button"
-              onClick={() => router.push('/settings/templates')}
-              className="text-sm text-[#71767e]"
-            >
-              {t('backToSettings')}
-            </button>
-          </div>
         </div>
       </div>
     )
@@ -945,6 +963,7 @@ export function InvoiceDesigner({
           onInsert={insertBefore}
           onPair={pairWith}
           pairable={COLUMN_ELIGIBLE_SECTIONS}
+          placeholderIds={placeholderIds}
           zoom={zoom}
           rulers={rulers}
         />
@@ -962,6 +981,7 @@ export function InvoiceDesigner({
           onTemplate={setTemplate}
           logoUrl={workshop.logoUrl}
           ownLogo={!!template.logoUrl}
+          sloganSet={!!companyWorkshop.slogan?.trim()}
           onLogo={(url) => setTemplate({ logoUrl: url })}
         />
       </div>

+ 39 - 1
src/features/invoice-designer/Render/SpecCanvas.tsx

@@ -241,6 +241,7 @@ export function SpecCanvas({
   onInsert,
   onPair,
   pairable,
+  placeholderIds,
   zoom,
   rulers,
 }: {
@@ -255,10 +256,17 @@ export function SpecCanvas({
   onPair: (id: string, side: 'left' | 'right', beforeId: string | null, afterId: string) => void
   /** Blocks that may share a row in columns. */
   pairable: ReadonlySet<string>
+  /**
+   * Blocks standing in for something the workshop has not filled in. Marked
+   * on the canvas because they read as real content otherwise, and print as
+   * nothing at all.
+   */
+  placeholderIds?: ReadonlySet<string>
   zoom: number
   rulers: boolean
 }) {
   const t = useTranslations('settings.designer')
+  const placeholderLabel = t('placeholder')
   const measureRef = useRef<HTMLDivElement>(null)
   const pagesRef = useRef<HTMLDivElement>(null)
   const containerRef = useRef<HTMLDivElement>(null)
@@ -842,12 +850,42 @@ export function SpecCanvas({
       style={{
         position: 'relative',
         cursor: 'grab',
-        outline: selected === block.id ? '2px solid #2563eb' : undefined,
+        outline:
+          selected === block.id
+            ? '2px solid #2563eb'
+            : // A stand-in for something the workshop has not filled in. It
+              // has to look unlike real content, because it reads as a real
+              // line otherwise and prints as nothing at all.
+              placeholderIds?.has(block.id)
+              ? '1px dashed #c9a227'
+              : undefined,
         outlineOffset: 2,
         ...textCss(block.text),
         ...style,
       }}
     >
+      {placeholderIds?.has(block.id) && selected !== block.id && !dragActive && (
+        <div
+          style={{
+            position: 'absolute',
+            top: -8,
+            right: -2,
+            background: '#fdf6e3',
+            border: '1px solid #e6d8a8',
+            color: '#8a6d1f',
+            fontFamily: "'IBM Plex Sans', sans-serif",
+            fontSize: 9,
+            fontWeight: 600,
+            padding: '1px 5px',
+            borderRadius: 3,
+            whiteSpace: 'nowrap',
+            pointerEvents: 'none',
+            zIndex: 6,
+          }}
+        >
+          {placeholderLabel}
+        </div>
+      )}
       {selected === block.id && !dragActive && (
         <div
           style={{