فهرست منبع

Let a workshop align the footer logo left, center or right (#296)

The footer printed its mark centered with no way to move it. Its own
logoAlign key on the section style moves the logo alone, so the closing
line and the portal link stay where a printed footer has always put them.
The control sits with the logo upload and appears once the logo field is
switched on, since there is nothing to align before that.
Bernt Christian Egeland 1 ماه پیش
والد
کامیت
6b601a916a

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

@@ -378,6 +378,9 @@ export function DesignerInspector({
         .filter((f) => !stored.some((existing) => existing.id === f.id))
         .map((f) => ({ id: f.id, visible: false })),
     ]
+    // The footer prints its mark only when the field is switched on, and the
+    // controls that dress that mark follow it.
+    const footerLogoOn = resolvedFields.some((f) => f.id === 'logo' && f.visible)
     const setFields = (fields: { id: string; visible: boolean }[]) =>
       onSection(section.id, { fields })
     const setStyle = (patch: InvoiceSectionStyle) => {
@@ -622,7 +625,28 @@ export function DesignerInspector({
           {section.id === 'footer' && (
             <Group title={t('logo')}>
               <LogoUpload value={logoUrl} own={ownLogo} onChange={onLogo} />
-              <p className="text-[11.5px] leading-snug text-[#8a8f97]">{t('footerLogoHint')}</p>
+              {/* There is nothing to align until the mark is switched on, so
+                  the control appears with it and the hint below tells a shop
+                  where the switch is until then. */}
+              {footerLogoOn && (
+                <div>
+                  <div className="mb-1.5 text-[13px] font-medium">{t('alignment')}</div>
+                  <Choice
+                    value={style.logoAlign ?? 'center'}
+                    options={[
+                      { value: 'left', label: t('columnLeft') },
+                      { value: 'center', label: t('alignCenter') },
+                      { value: 'right', label: t('columnRight') },
+                    ]}
+                    onChange={(logoAlign) =>
+                      setStyle({ logoAlign: logoAlign as 'left' | 'center' | 'right' })
+                    }
+                  />
+                </div>
+              )}
+              {!footerLogoOn && (
+                <p className="text-[11.5px] leading-snug text-[#8a8f97]">{t('footerLogoHint')}</p>
+              )}
             </Group>
           )}
 

+ 5 - 1
src/features/invoice-designer/Spec/buildSpec.ts

@@ -1464,6 +1464,10 @@ function footer(section: InvoiceSection, theme: DocumentTheme, data: DocumentDat
   const look = lookOf(section, theme)
   const size = look.fontSize ?? theme.fontSize
   const fields = sectionFields(section)
+  // The mark alone, not the closing line under it: a footer logo can sit hard
+  // left against the margin while the note and the portal link stay centered
+  // where a printed footer has always put them.
+  const logoAlign = section.style?.logoAlign ?? ('center' as const)
   const columns = [
     ['company_name', 'company_address'],
     ['company_phone', 'company_email'],
@@ -1488,7 +1492,7 @@ function footer(section: InvoiceSection, theme: DocumentTheme, data: DocumentDat
       src: data.logoUrl,
       maxWidth: 120,
       maxHeight: scale(size, 3.2),
-      align: 'center',
+      align: logoAlign,
     })
   }
   if (data.portalUrl) {

+ 4 - 0
src/features/settings/Schema/invoiceLayoutSchema.ts

@@ -43,6 +43,10 @@ export const invoiceSectionStyleSchema = z.object({
   /** Where the section sets its mark and lines. Unset follows the header
    *  style: compact leans left, standard right, modern centers. */
   align: z.enum(['left', 'center', 'right']).optional(),
+  /** Where a logo sits when the section prints one, on its own rather than
+   *  with the section's text: a footer mark can sit hard left under a margin
+   *  while the closing line stays centered. Unset centers it. */
+  logoAlign: z.enum(['left', 'center', 'right']).optional(),
   /** Room inside the section's panel, in points. Unset keeps each panel's
    *  own default, box or no box. */
   padding: z.number().min(0).max(40).optional(),