Files
test/.template/lib/presentation/components.typ
T
2026-10-09 04:24:38 +00:00

410 lines
11 KiB
Typst

// ==========================================
// PRESENTATION: ПЕРЕИСПОЛЬЗУЕМЫЕ КОМПОНЕНТЫ
// ==========================================
#import "../components.typ": corp_table, formula
#import "foundation.typ": render-media-slot
#import "../shared/numbering.typ": attachment_numbering
#let letter-accent = rgb("fbb20d")
// Большая информационная плашка для замечаний, пояснений и важных оговорок.
#let info-block(
body,
title: [ИНФОРМАЦИЯ],
fill: rgb("fbebec"),
accent: rgb("d73a49"),
text-fill: rgb("#402023"),
radius: 8pt,
inset: 8pt,
) = block(
width: 100%,
breakable: true,
fill: fill,
stroke: (left: 3pt + accent),
radius: radius,
inset: inset,
)[
#set par(first-line-indent: 0pt, justify: false)
#if title != none {
block(below: 0.35em)[
#text(fill: accent, weight: "bold", size: 9pt)[#title]
]
}
#text(fill: text-fill, size: 9pt)[#body]
]
#let footer-icon(name, color) = {
box(width: 20pt, height: 20pt, fill: color, radius: 2pt)[
#align(center + horizon)[
#image(
path("../assets/icons/" + name + ".png"),
width: 15pt,
height: 15pt,
fit: "contain",
)
]
]
}
#let company-logo(company, width: 4cm, height: 2.5cm, policy: "placeholder") = {
render-media-slot(
company.resources.logo,
policy: policy,
width: width,
height: height,
label: "ЛОГОТИП",
)
}
#let signing-media(stamp, signature, policy, show_stamp: true, offset: -1cm) = {
let has-missing-media = signature == none or (show_stamp and stamp == none)
let stamp-resource = if stamp == none and policy == "placeholder" {
path("../assets/placeholders/stamp.svg")
} else {
stamp
}
let signature-resource = if signature == none and policy == "placeholder" {
path("../assets/placeholders/signature.svg")
} else {
signature
}
if policy == "placeholder" and has-missing-media {
if show_stamp {
place(center, dx: -1.6cm, dy: offset)[
#render-media-slot(
stamp-resource,
policy: policy,
width: 2.8cm,
height: 2.8cm,
label: "ПЕЧАТЬ",
)
]
}
place(center, dx: if show_stamp { 1.6cm } else { 0cm }, dy: offset)[
#render-media-slot(
signature-resource,
policy: policy,
width: 2.8cm,
height: 1.4cm,
label: "ПОДПИСЬ",
)
]
} else {
if show_stamp {
place(center, dx: -0.5cm, dy: offset)[
#render-media-slot(
stamp-resource,
policy: policy,
width: 3.5cm,
height: 3.5cm,
label: "МЕСТО ПЕЧАТИ",
)
]
}
place(center, dx: if show_stamp { 1.5cm } else { 0cm }, dy: offset)[
#render-media-slot(
signature-resource,
policy: policy,
width: 3.5cm,
height: 2.5cm,
label: "МЕСТО ПОДПИСИ",
)
]
}
}
#let signature-block(
signer,
company: none,
media_policy: "placeholder",
show_stamp: true,
note: none,
) = {
let signature = signer.at(
"signature",
default: if company == none { none } else { company.resources.signature },
)
let stamp = signer.at(
"stamp",
default: if company == none { none } else { company.resources.stamp },
)
set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
let left-content = [
#signer.title
#if company != none { [\ #company.legal.name] }
]
let right-content = [#signer.name]
let media-content = signing-media(
stamp,
signature,
media_policy,
show_stamp: show_stamp,
)
v(2cm)
layout(size => {
let left-width = measure(left-content).width
let right-width = measure(right-content).width
let roomy = left-width + right-width + 5.5cm <= size.width
if roomy {
grid(
columns: (auto, 1fr, auto),
align: (left + bottom, center + bottom, right + bottom),
left-content,
media-content,
right-content,
)
} else {
grid(
columns: (1fr, 6.5cm, 1fr),
column-gutter: 8pt,
align: (left + bottom, center + bottom, right + bottom),
left-content,
media-content,
right-content,
)
}
})
if note != none {
text(font: "Arial", size: 7.5pt, fill: luma(90))[#note]
}
}
#let approval-block(approval, company, media_policy: "placeholder") = {
let signer = approval.signer
let signature = signer.at("signature", default: company.resources.signature)
let signature-resource = if signature == none and media_policy == "placeholder" {
path("../assets/placeholders/signature.svg")
} else {
signature
}
set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
box(width: 8cm)[
#align(right)[
#text(weight: "bold")[#approval.label] \
#signer.title \
#company.legal.name \
#v(0.2cm)
#render-media-slot(
signature-resource,
policy: media_policy,
width: 3cm,
height: 1.2cm,
label: "ПОДПИСЬ",
) \
#signer.name \
#if approval.date != none { approval.date }
]
]
}
#let company-footer(company) = {
let contacts = company.contacts
let banking = company.banking
let contact-lines = ()
if contacts.phone != none { contact-lines.push(contacts.phone) }
if contacts.email != none { contact-lines.push(contacts.email) }
if contacts.website != none { contact-lines.push(contacts.website) }
let id-lines = ()
for (key, label) in (
("inn", "ИНН"),
("kpp", "КПП"),
("ogrn", "ОГРН"),
("bin", "БИН"),
("kbe", "КБЕ"),
) {
let value = company.legal.at(key, default: none)
if value != none { id-lines.push([#label #value]) }
}
let banking-lines = ()
if banking.bank != none { banking-lines.push([Банк: #banking.bank]) }
if banking.at("correspondent-account", default: none) != none {
banking-lines.push([Кор. счёт: #banking.at("correspondent-account")])
}
if banking.bik != none { banking-lines.push([БИК: #banking.bik]) }
if contacts.at("address-en", default: none) != none {
banking-lines.push(contacts.at("address-en"))
}
set text(font: "Arial", size: 7.5pt, fill: luma(85))
set par(first-line-indent: 0pt, justify: false, leading: 0.2em)
grid(
columns: (1fr, 1.2fr),
column-gutter: 15pt,
[
#grid(
columns: (20pt, 1fr),
rows: (auto, auto),
column-gutter: 8pt,
row-gutter: 8pt,
if contact-lines.len() == 0 { none } else { footer-icon("email", company.brand.color) },
contact-lines.join([\ ]),
if contacts.address == none { none } else { footer-icon("location", company.brand.color) },
if contacts.address == none { none } else { contacts.address },
)
],
grid(
columns: (1fr, 20pt),
rows: (auto, auto),
column-gutter: 8pt,
row-gutter: 8pt,
align: (right, center + horizon),
align(right, id-lines.join([\ ])),
if id-lines.len() == 0 { none } else { footer-icon("card", company.brand.color) },
align(right, banking-lines.join([\ ])),
if banking-lines.len() == 0 { none } else { footer-icon("bank", company.brand.color) },
),
)
}
#let letter-header(
company,
recipient,
date: none,
reference: none,
title: none,
media_policy: "placeholder",
) = {
set par(first-line-indent: 0pt, justify: false, leading: 0.65em)
place(top + left, dx: 10cm, dy: -1.5cm)[
#rect(
width: 6.5cm,
height: 2cm,
fill: letter-accent,
radius: (bottom: 2pt),
)
]
v(30pt)
box(width: 4cm, height: 2.5cm)[
#company-logo(company, width: 4cm, height: 2.5cm, policy: media_policy)
]
v(-50pt)
v(2.5cm)
grid(
columns: (1.4fr, 1fr),
[
#if reference != none { [Исх. № #reference] }
#if date != none { [ от #date] }
],
[
#if recipient.company != none { [#recipient.company\ ] }
#if recipient.title != none { [#recipient.title\ ] }
#if recipient.name != none { recipient.name }
#if recipient.address != none { [\ #recipient.address] }
],
)
v(0.7cm)
if title != none {
align(center)[#text(weight: "bold")[#upper(title)]]
v(0.2cm)
}
}
#let attachment-list(attachments) = {
let items = if type(attachments) == dictionary { attachments.items } else { attachments }
if items.len() > 0 {
set par(first-line-indent: 0pt, justify: false)
for (index, item) in items.enumerate() {
let number = if item.number == auto { index + 1 } else { item.number }
[Приложение #number: #item.title]
if index < items.len() - 1 { linebreak() }
}
}
}
#let requisites-table(parties) = {
let field-labels = (
inn: "ИНН",
kpp: "КПП",
ogrn: "ОГРН",
bin: "БИН",
kbe: "КБе",
address: "Юридический адрес",
legal-address: "Юридический адрес",
phone: "Телефон",
email: "E-mail",
website: "Сайт",
bank: "Банк",
account: "Расчётный счёт",
settlement-account: "Расчётный счёт",
correspondent-account: "Корреспондентский счёт",
ks: "Корреспондентский счёт",
bik: "БИК",
iban: "IBAN",
)
let hidden-fields = (
"name",
"short-name",
"jurisdiction",
"summary",
"address-en",
"city",
)
let render-fields(fields) = {
for (key, value) in fields {
if value != none and key not in hidden-fields {
let label = field-labels.at(key, default: key)
[#label: #value\ ]
}
}
}
let columns = range(parties.len()).map(_ => 1fr)
table(
columns: columns,
stroke: 0.5pt,
inset: 6pt,
..parties.map(item => [
#set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
#text(weight: "bold")[#item.role] \
#item.name \
#render-fields(item.legal)
#render-fields(item.contacts)
#render-fields(item.banking)
]),
)
}
#let multi-party-signing(parties, media_policy: "reserve-space") = {
let columns = range(parties.len()).map(_ => 1fr)
grid(
columns: columns,
column-gutter: 16pt,
..parties.map(item => {
let person = item.representative
[
#set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
#if person != none {
grid(
columns: (1fr,),
rows: (auto, 0.35cm, 3.5cm, 0.25cm, auto),
[#text(weight: "bold")[#item.role] \ #person.title],
[],
block(width: 100%, height: 3.5cm)[
#signing-media(
person.stamp,
person.signature,
media_policy,
show_stamp: true,
offset: 0cm,
)
],
[],
[#person.name],
)
} else {
text(weight: "bold")[#item.role]
}
]
}),
)
}
#let appendix-heading(item, display-number) = {
set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
align(right)[Приложение #display-number]
v(0.2cm)
align(center)[
#text(weight: "bold")[#upper(item.title)]
#if item.subtitle != none { [\ #text(weight: "bold")[#item.subtitle]] }
]
}