انتقل إلى المحتوى الرئيسي
الوصفة رقم 162

دليل الوصفات · الفصل 1 · الصفحة والشبكة

هيكل تنظيمي وشبكة صور مبنيّان من وحدات

دليل خدمات بلدية يُنضَّد من وحدات: هيكل تنظيمي يرسم القالب خطوطه، وشبكات صور تكرّر بطاقة واحدة لكل مبنى.

في هذه الصفحة
المُخرَج
Canvas · PDF
المستوى
متقدم
Postext
اختُبرت مع Postext 1.48.1
تتطلب ≥ 1.48.1 · postext-pdf ≥ 1.48.1
الترخيص
حُدّثت في 20 أكتوبر 2026
الشيفرة MIT · النص CC BY 4.0
  • نموذج باللغة الإسبانية: لا توجد طبعة عربية بعد
  • مقاس القص 210 × 297 مم
  • عمود واحد
  • Source Serif 4 10.5/15
  • Public Sans
  • صفحتان
  • المستوى
  • Postext 1.48.1
  • أُخرجت في 98 ms
  • أسطر الشيفرة: 174

باختصار

صفحتان من دليل بلدية: من يتولّى ماذا، في رسم من مربعات تصلها خطوط، وأين تقع كل خدمة، في صفوف من الصور. تتألف الصفحتان من بضع كتل تُستعمل مرة بعد مرة، والـMarkdown يحدّد عددها.

ما الذي ستنضده

صفحتان متقابلتان من دليل خدمات بالديلوما، وهي بلدة متخيَّلة يسكنها 8400 نسمة. الصفحة اليسرى هي الهيكل التنظيمي للبلدية: رئيس البلدية والمجلس في مربع أحمر، وأربعة أقسام بالأخضر، وتحت كل قسم خدماته في مربعات فاتحة، تصلها خطوط خضراء تنتهي بنقطة أو بسهم. والصفحة اليمنى تبيّن أين تقع الخدمات: لوحتان في كل منهما ثلاث لوحات مائية، وتحت كل صورة الاسم والعنوان ومواعيد العمل، ومربع لمكتب خدمة السكان. لا شيء هنا مرسوم باليد. في القالب ثلاث وحدات (قسم، ولوحة صور، وتنبيه)، وتكدّس كل صفحة منها بقدر ما في الـMarkdown الخاص بها؛ والخطوط تتبع المربعات أينما دفعها النص.

تجيب هذه الوصفة عن

  • كيف أبني مخططًا تنظيميًا وشبكة صور من وحدات قابلة لإعادة الاستعمال، والقالب يرسم الخطوط؟

الجواب المختصر

script.js · الأسطر 40–72في الشيفرة الكاملة
// The page template draws the head of the chart and a group that takes the page's modules;
// the module draws one area: its box, a grid of its services and the lines between them.
const area = {
  id: 'area', role: 'module',
  frames: [
    { name: 'concejalia', style: 'area', width: mm(84), placement: at('container', 'top-left') },
    { name: 'servicio', style: 'servicio' }, // repeated: one per :::frame{slot="servicio"}
  ],
  groups: [{ name: 'servicios', kind: 'grid', columns: 3, columnGap: mm(4), rowGap: mm(4),
    repeat: { frames: ['servicio'] }, // rowAlign 'stretch': a row's boxes are one height
    placement: { ...at('#concejalia', 'below', 0, 7), size: { width: 'fill', height: 'fill' } } }],
  decoration: { elements: [
    // From the area down to each of its services: a tree with a bar over the row.
    line({ id: 'rama', routing: 'tree', from: { to: '#concejalia', side: 'bottom' },
      to: { to: '#servicio[*]', side: 'top' }, endCap: 'dot', capSize: mm(1.8) }),
    // From the mayor's box, a frame of the page, to this area: the spine and its branch.
    line({ id: 'tronco', routing: 'elbow', width: pt(1.8),
      from: { to: '#alcaldia', side: 'bottom', at: 0.06 }, to: { to: '#concejalia', side: 'left' },
      endCap: 'arrow', capSize: mm(2.6) }),
  ] },
};
const chart = {
  id: 'organigrama',
  frames: [
    { name: 'titulo', takes: 'h1', width: mm(W), placement: at('container', 'top-left') },
    { name: 'entradilla', flow: true, width: mm(W) },
    { name: 'alcaldia', style: 'alcaldia', width: mm(96) },
  ],
  groups: [{ name: 'cabeza', kind: 'stack', gap: mm(6),
    frames: ['titulo', 'entradilla', 'alcaldia'] },
    { name: 'areas', modules: true, gap: mm(6), placement: { ...at('#alcaldia', 'below', SPINE, 9),
      size: { width: mm(W - SPINE), height: 'fill' } } }],
};

المكونات

الخطوط
Public Sans, Source Serif 4 (SIL OFL 1.1)
الأصول
  • biblioteca-900.jpg
  • centro-mayores-900.jpg
  • mercado-900.jpg
  • musica-900.jpg
  • piscina-900.jpg
  • polideportivo-900.jpg
  • The reading room of a town library, in watercolour. (Generated With Diffusion Models, أصلي)
  • Children with violins and a piano at a music school, in watercolour. (Generated With Diffusion Models, أصلي)
  • Older people playing cards and painting at a day centre, in watercolour. (Generated With Diffusion Models, أصلي)
  • A covered pool under a timber roof, in watercolour. (Generated With Diffusion Models, أصلي)
  • Children playing basketball in a sports hall, in watercolour. (Generated With Diffusion Models, أصلي)
  • A Saturday market under the arcades of a town square, in watercolour. (Generated With Diffusion Models, أصلي)

طريقة التحضير

#1 · وحدة لكل قسم

script.js · الأسطر 40–72في الشيفرة الكاملة
// The page template draws the head of the chart and a group that takes the page's modules;
// the module draws one area: its box, a grid of its services and the lines between them.
const area = {
  id: 'area', role: 'module',
  frames: [
    { name: 'concejalia', style: 'area', width: mm(84), placement: at('container', 'top-left') },
    { name: 'servicio', style: 'servicio' }, // repeated: one per :::frame{slot="servicio"}
  ],
  groups: [{ name: 'servicios', kind: 'grid', columns: 3, columnGap: mm(4), rowGap: mm(4),
    repeat: { frames: ['servicio'] }, // rowAlign 'stretch': a row's boxes are one height
    placement: { ...at('#concejalia', 'below', 0, 7), size: { width: 'fill', height: 'fill' } } }],
  decoration: { elements: [
    // From the area down to each of its services: a tree with a bar over the row.
    line({ id: 'rama', routing: 'tree', from: { to: '#concejalia', side: 'bottom' },
      to: { to: '#servicio[*]', side: 'top' }, endCap: 'dot', capSize: mm(1.8) }),
    // From the mayor's box, a frame of the page, to this area: the spine and its branch.
    line({ id: 'tronco', routing: 'elbow', width: pt(1.8),
      from: { to: '#alcaldia', side: 'bottom', at: 0.06 }, to: { to: '#concejalia', side: 'left' },
      endCap: 'arrow', capSize: mm(2.6) }),
  ] },
};
const chart = {
  id: 'organigrama',
  frames: [
    { name: 'titulo', takes: 'h1', width: mm(W), placement: at('container', 'top-left') },
    { name: 'entradilla', flow: true, width: mm(W) },
    { name: 'alcaldia', style: 'alcaldia', width: mm(96) },
  ],
  groups: [{ name: 'cabeza', kind: 'stack', gap: mm(6),
    frames: ['titulo', 'entradilla', 'alcaldia'] },
    { name: 'areas', modules: true, gap: mm(6), placement: { ...at('#alcaldia', 'below', SPINE, 9),
      size: { width: mm(W - SPINE), height: 'fill' } } }],
};

القالب الذي يحمل role: 'module' وحدةٌ: بضعة إطارات ومجموعات وزخارف معدّة لتُستعمل من جديد. في صفحة الهيكل التنظيمي مجموعة تحمل modules: true، وكل :::module{template="area"} في الـMarkdown الخاص بها يضع قسمًا آخر تحت الذي قبله، على بعد 6 مم. تأخذ إطارات الوحدة اسم النسخة، area[1].concejalia وarea[2].concejalia…، فيكفي أن يُكتب موصلاها مرة واحدة: rama شجرة (tree) من أسفل القسم إلى كل خدمة في شبكته (#servicio[*])، وtronco خط منكسر (elbow) من نقطة قريبة من يسار مربع رئيس البلدية (at: 0.06) إلى الجانب الأيسر من القسم. #alcaldia إطار من إطارات الصفحة لا من إطارات الوحدة: تحتفظ الوحدة بهذا الطرف ويجده التنضيد في الصفحة، فتشترك الخطوط المنكسرة الأربعة في مقطعها الأول وتُقرأ جذعًا واحدًا يمدّ سهمًا إلى كل قسم. والخدمات شبكة من ثلاثة أعمدة تكرّر إطار servicio، فالقسم الذي له خدمتان يرسم مربعين، ولشجرته فرعان.

#2 · لوحات من بطاقات الصور

script.js · الأسطر 76–105في الشيفرة الكاملة
const gallery = {
  id: 'galeria', role: 'module',
  frames: [
    { name: 'cabecera', takes: 'h2', placement: at('container', 'top-left', 4, 4) },
    { name: 'foto', style: 'marco' }, // the photo of a card
    { name: 'pie', style: 'ficha', placement: at('#foto', 'below', 0, 2.5) }, // and its words
  ],
  groups: [{ name: 'fotos', kind: 'grid', columns: 3, columnGap: mm(5), rowGap: mm(6),
    rowAlign: 'start', repeat: { frames: ['foto', 'pie'] },
    placement: { ...at('#cabecera', 'below', 0, 3), size: { width: mm(W - 8), height: 'fill' } } }],
  // The module's own ground: a tint as large as the module, whatever its rows.
  decoration: { elements: [{ kind: 'box', id: 'suelo', decorative: true,
    placement: { ...at('container', 'top-left'), size: { width: 'fill', height: 'fill' } },
    style: { backgroundColor: col('tint'), borderRadius: mm(3) } }] },
};
// A notice box as a module of its own: one frame whose tab grows out of the box (joined).
const notice = { id: 'aviso', role: 'module', frames: [{ name: 'texto', style: 'aviso',
  placement: at('container', 'top-left', 0, 7) }] };
const places = {
  id: 'centros',
  frames: [
    { name: 'titulo', takes: 'h1', width: mm(W), placement: at('container', 'top-left') },
    { name: 'entradilla', flow: true, width: mm(W) },
    { name: 'colofon', style: 'colofon', optional: true, width: mm(W),
      placement: at('container', 'bottom-left', 0, 8) },
  ],
  groups: [{ name: 'cabeza', kind: 'stack', gap: mm(5), frames: ['titulo', 'entradilla'] },
    { name: 'galerias', modules: true, gap: mm(7), placement: { ...at('#entradilla', 'below', 0, 7),
      size: { width: mm(W), height: 'fill' } } }],
};

اللوحة أيضًا وحدة: إطار رأس يأخذ عنوان ## الخاص بالوحدة، وشبكة تكرّر زوجًا من الإطارات، الصورة والكلمات التي تحتها. كل :::frame{slot="foto"} بطاقة، و:::frame{slot="pie"} الذي بداخله يذهب إلى كلمات تلك البطاقة. الأرضية الملوّنة زخرفة من زخارف الوحدة، مربع بحجم الوحدة، فتنمو مع الصفوف ومع أطول تعليق. ومكتب خدمة السكان وحدة ثالثة، مربع لسانه joined: اللسان والمربع حدّ واحد بتعبئة واحدة.

#3 · صور بمقاس الطباعة

script.js · الأسطر 346–372في الشيفرة الكاملة
const ART = {
  biblioteca: t({ es: 'La sala de lectura de la biblioteca, con estanterías de madera y vecinos '
      + 'de todas las edades leyendo en mesas largas.',
    en: 'The library reading room, with wooden shelves and neighbours of every age reading at '
      + 'long tables.' }),
  musica: t({ es: 'Un aula de la escuela de música: niños con violines, una profesora y una niña '
      + 'al piano.',
    en: 'A room of the music school: children with violins, a teacher and a girl at the piano.' }),
  'centro-mayores': t({ es: 'El centro de día: mayores jugando a las cartas y pintando junto a '
      + 'una puerta al jardín.',
    en: 'The day centre: older people playing cards and painting beside a door to the garden.' }),
  piscina: t({ es: 'La piscina cubierta bajo un techo de madera, con nadadores en las calles y un '
      + 'socorrista.',
    en: 'The covered pool under a timber roof, with swimmers in the lanes and a lifeguard.' }),
  polideportivo: t({ es: 'La pista del polideportivo, con niños jugando al baloncesto y gradas a '
      + 'un lado.',
    en: 'The sports hall court, with children playing basketball and stands along one side.' }),
  mercado: t({ es: 'El mercado de los sábados en los soportales de la plaza, con el ayuntamiento '
      + 'al fondo.',
    en: 'The Saturday market under the arcades of the square, with the town hall behind.' }),
};
const resources = await Promise.all(Object.entries(ART).map(async ([id, altText]) => {
  const fileId = `${id}-900.jpg`;
  await loadImage(fileId, asset(fileId));
  return { id, typeId: 'foto', kind: 'bitmap', caption: '', altText, createdAt: 0, updatedAt: 0,
    bitmap: { fileId, format: 'jpeg', width: 900, height: 600, resolution: 400 } };
}));

اللوحات المائية الست بمقاس 900 × 600 بكسل، مصرَّح بها بدقة 400 نقطة في البوصة، أي بعرض 57 مم، وهو تقريبًا عرض عمود من أعمدة الشبكة، فلا تكبّرها الشبكة أبدًا. نصها البديل هنا في السكربت لأن الصور واحدة في الطبعتين؛ أما التعليق الذي يتغيّر بتغيّر اللغة فمكانه سطر ::caption في الـMarkdown.

#4 · أخضر واحد للخطوط والمربعات

script.js · الأسطر 15–28في الشيفرة الكاملة
const palette = {
  ink: '#1f2b2a', // text
  green: '#1d6b57', // the lines of the chart, the areas' boxes, the headings
  mayor: '#a8452e', // the box at the head of the chart
  tint: '#e6f1ec', // the services' boxes and the galleries' ground
  rule: '#b9cbc3', // the hairline round a service
  muted: '#5c6b67', // running heads and the colophon
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'green (defaults)', value: { hex: palette.green, model: 'hex' } },
];

تستعمل الموصلات ومربعات الأقسام ورؤوس اللوحات اللون green، وهو مدخل من مداخل لوحة الألوان، فالبلدة التي لها لون آخر تغيّر قيمة ست عشرية واحدة. ويحمل مربع رئيس البلدية اللون الدافئ الوحيد على الصفحتين، في الموضع الذي يقع عليه نظر القارئ أولًا.

الوصفة كاملة

Sandbox
// ═══ Postext Cookbook · Nº 162 · An org chart and a photo grid built from modules ═══
// https://postext.dev/en/cookbook/org-chart-from-modules
// Code: MIT · Text: original (CC BY 4.0) · Pictures: generated with diffusion models
// Fonts: Public Sans, Source Serif 4 (SIL OFL 1.1) · Needs postext ≥ 1.48.1
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'es'; // @lang: the language of the sample document ('es' | 'en')
const RECIPE = 'org-chart-from-modules';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: an ink, the town's green for the lines and the areas, a warm mayor's box
const palette = {
  ink: '#1f2b2a', // text
  green: '#1d6b57', // the lines of the chart, the areas' boxes, the headings
  mayor: '#a8452e', // the box at the head of the chart
  tint: '#e6f1ec', // the services' boxes and the galleries' ground
  rule: '#b9cbc3', // the hairline round a service
  muted: '#5c6b67', // running heads and the colophon
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'green (defaults)', value: { hex: palette.green, model: 'hex' } },
];
// #endregion

const SANS = 'Public Sans', SERIF = 'Source Serif 4';
const PAGE = { w: 210, h: 297, top: 22, bottom: 22, inner: 18, outer: 15 }; // mm, mirrored
const W = PAGE.w - PAGE.inner - PAGE.outer; // the type area's width: 177 mm
const SPINE = 12; // mm: the room on the left of the areas where the chart's spine runs down
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const line = (more) => ({ kind: 'connector', color: col('green'), width: pt(1.2), radius: mm(2.5),
  ...more });

// #region answer: a module per area of the town hall, stacked by the page's modules group
// The page template draws the head of the chart and a group that takes the page's modules;
// the module draws one area: its box, a grid of its services and the lines between them.
const area = {
  id: 'area', role: 'module',
  frames: [
    { name: 'concejalia', style: 'area', width: mm(84), placement: at('container', 'top-left') },
    { name: 'servicio', style: 'servicio' }, // repeated: one per :::frame{slot="servicio"}
  ],
  groups: [{ name: 'servicios', kind: 'grid', columns: 3, columnGap: mm(4), rowGap: mm(4),

يعمل ملف script.js المجمّع كما هو: الصقه في سكربت الوحدة (module) لأي صفحة، أو افتح الوصفة على CodePen. مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)

تنويعات

#عمودان للخدمات

مع columns: 2 تصطف خدمات كل قسم اثنتين اثنتين؛ والشجرة لا ترسم شريطها إلا فوق الصف الأول، فاحتفظ بهذا للأقسام التي لها خدمتان أو أربع.

-  groups: [{ name: 'servicios', kind: 'grid', columns: 3, columnGap: mm(4), rowGap: mm(4),
+  groups: [{ name: 'servicios', kind: 'grid', columns: 2, columnGap: mm(4), rowGap: mm(4),

#دوِّر زوايا الجذع

يدوّر radius منعطفات الخط المنكسر والشجرة: تعطي الدالة المساعدة line() كل موصل 2.5 مم؛ ومع radius: mm(0) تصير زوايا الهيكل التنظيمي قائمة.

أخطاء شائعة

  • عنصر الشبكة حيّز قائم بذاته: كلمات بطاقة الصورة لا تنتمي إليها إلا إذا كُتب :::frame{slot="pie"} الخاص بها داخل :::frame{slot="foto"} الخاص بالبطاقة.
  • موصل الوحدة الذي يسمّي إطارًا من إطارات الصفحة لا يُرسم إلا في الصفحات التي فيها ذلك الإطار؛ ويُحذف في غيرها، ويذكر سجل إعادة التنضيد connectorDropped.
  • اللسان الموصول يقع على حافة المربع ويأخذ تعبئته: أعطِ كلماته color داكنًا، فالتسمية البيضاء على مربع فاتح لا تُقرأ.

الحقوق

الوصفة
Ignacio Ferro
النص
  • A guide to the council services of Valdeloma, an invented town, written for this recipe in Spanish and English · Postext Cookbook · CC BY 4.0
الصور
  • The reading room of a town library, in watercolour. · Generated With Diffusion Models · أصلي
  • Children with violins and a piano at a music school, in watercolour. · Generated With Diffusion Models · أصلي
  • Older people playing cards and painting at a day centre, in watercolour. · Generated With Diffusion Models · أصلي
  • A covered pool under a timber roof, in watercolour. · Generated With Diffusion Models · أصلي
  • Children playing basketball in a sports hall, in watercolour. · Generated With Diffusion Models · أصلي
  • A Saturday market under the arcades of a town square, in watercolour. · Generated With Diffusion Models · أصلي
الخطوط
Public Sans (SIL OFL 1.1) · Source Serif 4 (SIL OFL 1.1)
الشيفرة
MIT، مثل Postext
SandboxPDF