diff --git a/CHANGELOG.md b/CHANGELOG.md index cef198cb3..f2e77d060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,36 @@ follow semantic versioning; release dates are ISO 8601. ### Templates +- **A clinical CV preset in five bands: `TealPulse`.** A one-page sheet whose mark is a + heart crossed by a flat pulse, beside a letter-spaced name over a contact strip + divided by short rules; then a two-column body carrying the competencies as dotted + lines beside the summary and the roles under badged headings; then a three-column + closing band for the degree, the certifications and the facts; and a tracked line + under a rule that ends in a small heart. Ships as `cv.presets.TealPulse` on the + existing `CvDocument` model with **no model change at all**, porting the rendered + layout of a published standalone template. **It sets its own page**, unlike every CV + preset before it: the design was drawn on a raster whose proportion is not A4's and + every length is a share of that grid, so a caller's page size and margin are + overwritten rather than followed. Six berths reach their sections by title, including + a tagline berth whose body is drawn and whose title is not — the title is how a + document names the berth. Both vertical rules are the left border of the column to + their right rather than lines placed beside them, so they are the grid: change a + weight and the rules follow. The main headings are laid over their own rule with the + paper knocked out behind every letter, because a row cannot nest in a row cell and the + rule's visible length still has to follow the words. **What happens past one page is + not what the other ported CV presets do:** the bands are stacked in the page flow, so + a CV with more roles than the design holds carries the closing band onto a second page + rather than losing anything, while the body row itself is atomic and a body taller + than a page raises `AtomicNodeTooLargeException`. As on `TerracottaRail`, a link is + drawn as its own label with the address behind it, which is the one deliberate + departure from the ported sheet — writing the URL out makes the strip's gaps depend on + how long a profile is called — measured at six of 121 nodes shifting sideways and + 7 724 of 1 987 720 pixels, with nothing moving vertically. Guarded by a smoke test + (including the empty document, the page override, the tagline's title staying off the + sheet, the link targets, a linked role and degree, the run onto a second page and the + refusal of a body taller than one), an exact layout snapshot and a pixel-parity gate; + the examples showcase gains `cv-teal-pulse-v2`. + - **An architect's two-column CV preset: `TerracottaRail`.** A one-page A4 sheet whose narrow column carries a serif monogram over a terracotta rule, the contact channels behind their marks, two bulleted lists and a block of closing facts, beside a wide diff --git a/assets/readme/examples/cv-teal-pulse-v2.pdf b/assets/readme/examples/cv-teal-pulse-v2.pdf new file mode 100644 index 000000000..73b531b29 Binary files /dev/null and b/assets/readme/examples/cv-teal-pulse-v2.pdf differ diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java index 398cd51e9..dbecd3306 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -80,6 +80,7 @@ import com.demcha.examples.templates.coverletter.CvSidebarPortraitLetterV2Example; import com.demcha.examples.templates.coverletter.CvTimelineMinimalLetterV2Example; import com.demcha.examples.templates.cv.v2.CharcoalGoldExample; +import com.demcha.examples.templates.cv.v2.TealPulseExample; import com.demcha.examples.templates.cv.v2.TerracottaRailExample; import com.demcha.examples.templates.cv.v2.CvBlueBannerExample; import com.demcha.examples.templates.cv.v2.CvBoxedV2Example; @@ -156,6 +157,7 @@ public static void main(String[] args) throws Exception { System.out.println("Generated: " + SerifHeadlineExample.generate()); System.out.println("Generated: " + CharcoalGoldExample.generate()); System.out.println("Generated: " + TerracottaRailExample.generate()); + System.out.println("Generated: " + TealPulseExample.generate()); // Cover letters (v2 layered — 15 paired letters, one per CV preset) System.out.println("Generated: " + CvBlueBannerLetterV2Example.generate()); diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java index 9df177429..0d2b08574 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java @@ -65,6 +65,7 @@ record Entry(String title, String description, List tags, String codeUrl cv("cv-serif-headline-v2", "SerifHeadlineExample", "Serif Headline", "Volkhov masthead over a two-column body: roles on a timeline rail and marked project cards beside degrees and skill meters, closing with full-width certification and achievement bands.", "serif", "two-column"); cv("cv-charcoal-gold-v2", "CharcoalGoldExample", "Charcoal Gold", "Charcoal sidebar with a ringed photograph, rated skills and languages, beside a paper column with a two-tone name, a dated experience rail and paired credential columns.", "sidebar", "photo", "gold"); cv("cv-terracotta-rail-v2", "TerracottaRailExample", "Terracotta Rail", "Serif monogram over a terracotta rule, contact channels behind their marks and two bulleted lists, beside a letter-spaced masthead, roles on a ringed rail, a projects grid and the degrees.", "sidebar", "monogram", "terracotta"); + cv("cv-teal-pulse-v2", "TealPulseExample", "Teal Pulse", "Clinical sheet in five bands: a heart crossed by a pulse beside the name, a contact strip on rules, competencies beside the summary and roles, and a three-column closing band over a tracked tagline.", "clinical", "badges", "teal"); // ===== Templates / Cover Letter (v2 layered, paired 1:1 with CV) ===== // Registered directly: letter() points at the layered preset examples under diff --git a/examples/src/main/java/com/demcha/examples/support/TealPulseSampleData.java b/examples/src/main/java/com/demcha/examples/support/TealPulseSampleData.java new file mode 100644 index 000000000..1022015bd --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/support/TealPulseSampleData.java @@ -0,0 +1,167 @@ +package com.demcha.examples.support; + +import com.demcha.compose.document.templates.core.identity.Contact; +import com.demcha.compose.document.templates.core.identity.Link; +import com.demcha.compose.document.templates.cv.data.CvDocument; +import com.demcha.compose.document.templates.cv.data.CvEntry; +import com.demcha.compose.document.templates.cv.data.CvIdentity; +import com.demcha.compose.document.templates.cv.data.CvName; +import com.demcha.compose.document.templates.cv.data.CvSkill; +import com.demcha.compose.document.templates.cv.data.EntriesSection; +import com.demcha.compose.document.templates.cv.data.ParagraphSection; +import com.demcha.compose.document.templates.cv.data.SkillGroup; +import com.demcha.compose.document.templates.cv.data.SkillsSection; +import com.demcha.compose.document.templates.cv.data.Slot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalDouble; + +/** + * Shared sample data for the Teal Pulse CV example. + * + *

The sample is the preset's reference content: a nurse's one-page CV with + * four contact channels including a link, twelve competencies, a summary, + * three roles with their highlights, a degree, four certifications, three + * facts and the closing tagline.

+ * + *

Kept in lockstep with the qa module's {@code TealPulseFixtures} — the + * two modules cannot share a source file, so a content change here belongs + * there too.

+ */ +public final class TealPulseSampleData { + + /** The break an entry stacks its highlights on. */ + private static final String NEWLINE = String.valueOf((char) 10); + + /** The dash this sheet writes a date range with. */ + private static final String EN_DASH = String.valueOf((char) 0x2013); + + private TealPulseSampleData() { + } + + /** The single-page reference CV. */ + public static CvDocument sample() { + List placements = new ArrayList<>(); + placements.add(new CvDocument.Placement(Slot.SIDEBAR, competencies())); + placements.add(new CvDocument.Placement(Slot.MAIN, summary())); + placements.add(new CvDocument.Placement(Slot.MAIN, experience())); + placements.add(new CvDocument.Placement(Slot.FOOTER, education())); + placements.add(new CvDocument.Placement(Slot.FOOTER, certifications())); + placements.add(new CvDocument.Placement(Slot.FOOTER, facts())); + placements.add(new CvDocument.Placement(Slot.FOOTER, tagline())); + return new CvDocument(identity(), placements); + } + + private static CvIdentity identity() { + return new CvIdentity( + CvName.of("ISABELLA", "MOORE"), + "REGISTERED NURSE", + new Contact("+44 7700 900123", "isabella.moore@email.com", + "Manchester, United Kingdom"), + List.of(new Link("LinkedIn", "https://www.linkedin.com/in/isabellamoore-rn")), + Optional.empty()); + } + + private static SkillsSection competencies() { + List entries = new ArrayList<>(); + for (String name : new String[] { + "Patient Assessment", "Care Planning", "Medication Administration", + "Clinical Documentation", "Infection Prevention", "IV Therapy", + "Wound Care", "Patient Education", "Safeguarding", "Team Collaboration", + "Discharge Coordination", "Electronic Health Records"}) { + entries.add(new CvSkill(name, OptionalDouble.empty())); + } + return new SkillsSection("CORE COMPETENCIES", + List.of(new SkillGroup("CORE COMPETENCIES", entries))); + } + + private static ParagraphSection summary() { + return new ParagraphSection("PROFESSIONAL SUMMARY", + "Compassionate Registered Nurse with 7+ years of experience delivering " + + "high-quality patient care across acute medical, surgical, and " + + "community settings. Skilled in patient assessment, medication " + + "administration, multidisciplinary collaboration, discharge " + + "planning, and maintaining accurate clinical documentation. Known " + + "for calm decision-making, strong communication, and patient-centred " + + "care."); + } + + private static EntriesSection experience() { + return new EntriesSection("PROFESSIONAL EXPERIENCE", List.of( + CvEntry.builder("Senior Staff Nurse") + .subtitle("Manchester Royal Infirmary, Manchester, UK") + .date("2021" + EN_DASH + "Present") + .body(String.join(NEWLINE, + "Deliver direct nursing care for adult patients on a busy " + + "acute medical ward.", + "Coordinate care plans with doctors, therapists, and support " + + "staff to improve outcomes.", + "Supervise junior nurses and support student placements during " + + "clinical rotations.", + "Improved discharge coordination and patient communication " + + "across the ward.", + "Maintain precise documentation and ensure compliance with " + + "clinical standards.")) + .build(), + CvEntry.builder("Staff Nurse") + .subtitle("Salford General Hospital, Salford, UK") + .date("2018" + EN_DASH + "2021") + .body(String.join(NEWLINE, + "Provided nursing care across medical and surgical units.", + "Administered medications, monitored vital signs, and escalated " + + "deteriorating patients promptly.", + "Supported infection control procedures and safe discharge " + + "planning.", + "Built strong rapport with patients and families during " + + "treatment and recovery.")) + .build(), + CvEntry.builder("Community Nurse") + .subtitle("NorthCare Community Health, Greater Manchester, UK") + .date("2016" + EN_DASH + "2018") + .body(String.join(NEWLINE, + "Delivered home-based nursing care and patient education.", + "Managed wound care, medication support, and follow-up visits " + + "for vulnerable patients.", + "Worked closely with GPs, social services, and families to " + + "coordinate holistic care.")) + .build())); + } + + private static EntriesSection education() { + return new EntriesSection("EDUCATION", List.of( + CvEntry.builder("BSc (Hons) Adult Nursing") + .subtitle("University of Manchester") + .date("2013" + EN_DASH + "2016") + .build())); + } + + private static EntriesSection certifications() { + List entries = new ArrayList<>(); + for (String name : new String[] { + "NMC Registered Nurse", "Immediate Life Support (ILS)", + "Venepuncture and Cannulation Certification", "Safeguarding Adults Level 3"}) { + entries.add(CvEntry.builder(name).build()); + } + return new EntriesSection("CERTIFICATIONS", entries); + } + + private static EntriesSection facts() { + return new EntriesSection("ADDITIONAL INFORMATION", List.of( + CvEntry.builder("Languages:") + .body("English (Native), Polish (Conversational)") + .build(), + CvEntry.builder("Right to Work:").body("United Kingdom").build(), + CvEntry.builder("Availability:").body("1 month notice").build())); + } + + /** + * The closing line. Its title names the berth and is not drawn — only the + * body reaches the sheet. + */ + private static ParagraphSection tagline() { + return new ParagraphSection("TAGLINE", + "COMPASSIONATE CARE. CLINICAL EXCELLENCE. BETTER OUTCOMES."); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/cv/v2/TealPulseExample.java b/examples/src/main/java/com/demcha/examples/templates/cv/v2/TealPulseExample.java new file mode 100644 index 000000000..35243ac00 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/cv/v2/TealPulseExample.java @@ -0,0 +1,51 @@ +package com.demcha.examples.templates.cv.v2; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.cv.data.CvDocument; +import com.demcha.compose.document.templates.cv.presets.TealPulse; +import com.demcha.examples.support.ExampleOutputPaths; +import com.demcha.examples.support.TealPulseSampleData; + +import java.nio.file.Path; + +/** + * Renders the layered {@code cv.v2} Teal Pulse preset against the nursing + * sample. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/cv/cv-teal-pulse-v2.pdf}.

+ * + *

The preset owns its page geometry — the design is drawn on a raster + * whose proportion is not A4's — so the session starts unconfigured.

+ */ +public final class TealPulseExample { + + private TealPulseExample() { + } + + /** + * @return absolute path of the rendered PDF + * @throws Exception if rendering fails + */ + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare("templates/cv", "cv-teal-pulse-v2.pdf"); + CvDocument doc = TealPulseSampleData.sample(); + DocumentTemplate template = TealPulse.create(); + + try (DocumentSession document = GraphCompose.document(outputFile).create()) { + template.compose(document, doc); + document.buildPdf(); + } + return outputFile; + } + + /** + * @param args ignored + * @throws Exception if rendering fails + */ + public static void main(String[] args) throws Exception { + System.out.println("Generated: " + generate()); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseFixtures.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseFixtures.java new file mode 100644 index 000000000..d9138f809 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseFixtures.java @@ -0,0 +1,166 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.templates.core.identity.Contact; +import com.demcha.compose.document.templates.core.identity.Link; +import com.demcha.compose.document.templates.cv.data.CvDocument; +import com.demcha.compose.document.templates.cv.data.CvEntry; +import com.demcha.compose.document.templates.cv.data.CvIdentity; +import com.demcha.compose.document.templates.cv.data.CvName; +import com.demcha.compose.document.templates.cv.data.CvSkill; +import com.demcha.compose.document.templates.cv.data.EntriesSection; +import com.demcha.compose.document.templates.cv.data.ParagraphSection; +import com.demcha.compose.document.templates.cv.data.SkillGroup; +import com.demcha.compose.document.templates.cv.data.SkillsSection; +import com.demcha.compose.document.templates.cv.data.Slot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalDouble; + +/** + * Shared fixture data for the {@link TealPulse} gates — the SAME document + * feeds the pixel parity test and the layout snapshot test, so a geometry + * shift the pixel budget absorbs still trips the exact snapshot, and vice + * versa. + */ +final class TealPulseFixtures { + + /** The break an entry stacks its highlights on. */ + private static final String NEWLINE = String.valueOf((char) 10); + + /** The dash this sheet writes a date range with. */ + private static final String EN_DASH = String.valueOf((char) 0x2013); + + private TealPulseFixtures() { + } + + /** + * The canonical one-page CV — the brand mark, four contact channels + * including a link, twelve competencies, a summary, three roles with their + * highlights, a degree, four certifications, three facts and the closing + * tagline. + */ + static CvDocument canonicalCv() { + List placements = new ArrayList<>(); + placements.add(new CvDocument.Placement(Slot.SIDEBAR, competencies())); + placements.add(new CvDocument.Placement(Slot.MAIN, summary())); + placements.add(new CvDocument.Placement(Slot.MAIN, experience())); + placements.add(new CvDocument.Placement(Slot.FOOTER, education())); + placements.add(new CvDocument.Placement(Slot.FOOTER, certifications())); + placements.add(new CvDocument.Placement(Slot.FOOTER, facts())); + placements.add(new CvDocument.Placement(Slot.FOOTER, tagline())); + return new CvDocument(identity(), placements); + } + + static CvIdentity identity() { + return new CvIdentity( + CvName.of("ISABELLA", "MOORE"), + "REGISTERED NURSE", + new Contact("+44 7700 900123", "isabella.moore@email.com", + "Manchester, United Kingdom"), + List.of(new Link("LinkedIn", "https://www.linkedin.com/in/isabellamoore-rn")), + Optional.empty()); + } + + static SkillsSection competencies() { + List entries = new ArrayList<>(); + for (String name : new String[] { + "Patient Assessment", "Care Planning", "Medication Administration", + "Clinical Documentation", "Infection Prevention", "IV Therapy", + "Wound Care", "Patient Education", "Safeguarding", "Team Collaboration", + "Discharge Coordination", "Electronic Health Records"}) { + entries.add(new CvSkill(name, OptionalDouble.empty())); + } + return new SkillsSection("CORE COMPETENCIES", + List.of(new SkillGroup("CORE COMPETENCIES", entries))); + } + + static ParagraphSection summary() { + return new ParagraphSection("PROFESSIONAL SUMMARY", + "Compassionate Registered Nurse with 7+ years of experience delivering " + + "high-quality patient care across acute medical, surgical, and " + + "community settings. Skilled in patient assessment, medication " + + "administration, multidisciplinary collaboration, discharge " + + "planning, and maintaining accurate clinical documentation. Known " + + "for calm decision-making, strong communication, and patient-centred " + + "care."); + } + + static EntriesSection experience() { + return new EntriesSection("PROFESSIONAL EXPERIENCE", List.of( + CvEntry.builder("Senior Staff Nurse") + .subtitle("Manchester Royal Infirmary, Manchester, UK") + .date("2021" + EN_DASH + "Present") + .body(String.join(NEWLINE, + "Deliver direct nursing care for adult patients on a busy " + + "acute medical ward.", + "Coordinate care plans with doctors, therapists, and support " + + "staff to improve outcomes.", + "Supervise junior nurses and support student placements during " + + "clinical rotations.", + "Improved discharge coordination and patient communication " + + "across the ward.", + "Maintain precise documentation and ensure compliance with " + + "clinical standards.")) + .build(), + CvEntry.builder("Staff Nurse") + .subtitle("Salford General Hospital, Salford, UK") + .date("2018" + EN_DASH + "2021") + .body(String.join(NEWLINE, + "Provided nursing care across medical and surgical units.", + "Administered medications, monitored vital signs, and escalated " + + "deteriorating patients promptly.", + "Supported infection control procedures and safe discharge " + + "planning.", + "Built strong rapport with patients and families during " + + "treatment and recovery.")) + .build(), + CvEntry.builder("Community Nurse") + .subtitle("NorthCare Community Health, Greater Manchester, UK") + .date("2016" + EN_DASH + "2018") + .body(String.join(NEWLINE, + "Delivered home-based nursing care and patient education.", + "Managed wound care, medication support, and follow-up visits " + + "for vulnerable patients.", + "Worked closely with GPs, social services, and families to " + + "coordinate holistic care.")) + .build())); + } + + static EntriesSection education() { + return new EntriesSection("EDUCATION", List.of( + CvEntry.builder("BSc (Hons) Adult Nursing") + .subtitle("University of Manchester") + .date("2013" + EN_DASH + "2016") + .build())); + } + + static EntriesSection certifications() { + List entries = new ArrayList<>(); + for (String name : new String[] { + "NMC Registered Nurse", "Immediate Life Support (ILS)", + "Venepuncture and Cannulation Certification", "Safeguarding Adults Level 3"}) { + entries.add(CvEntry.builder(name).build()); + } + return new EntriesSection("CERTIFICATIONS", entries); + } + + static EntriesSection facts() { + return new EntriesSection("ADDITIONAL INFORMATION", List.of( + CvEntry.builder("Languages:") + .body("English (Native), Polish (Conversational)") + .build(), + CvEntry.builder("Right to Work:").body("United Kingdom").build(), + CvEntry.builder("Availability:").body("1 month notice").build())); + } + + /** + * The closing line. Its title names the berth and is not drawn — only the + * body reaches the sheet. + */ + static ParagraphSection tagline() { + return new ParagraphSection("TAGLINE", + "COMPASSIONATE CARE. CLINICAL EXCELLENCE. BETTER OUTCOMES."); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseLayoutSnapshotTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseLayoutSnapshotTest.java new file mode 100644 index 000000000..2725dc099 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseLayoutSnapshotTest.java @@ -0,0 +1,32 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.TemplateTestSupport; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Exact layout snapshot gate for {@link TealPulse} — freezes the resolved + * geometry of the canonical one-page CV, which the pixel budget cannot see: a + * small column or spacing shift stays under the visual-diff budget but changes + * the snapshot. + * + *

The preset owns its page geometry, so the session starts unconfigured.

+ * + *

Refresh with {@code -Dgraphcompose.updateSnapshots=true} after a + * deliberate layout change, and commit the JSON with the change.

+ */ +class TealPulseLayoutSnapshotTest { + + @Test + void canonicalCvMatchesLayoutSnapshot() throws Exception { + try (DocumentSession session = GraphCompose.document().create()) { + TealPulse.create().compose(session, TealPulseFixtures.canonicalCv()); + assertThat(session.layoutSnapshot().totalPages()).isEqualTo(1); + TemplateTestSupport.assertCanonicalSnapshot( + session, "teal_pulse_layout", "cv-v2"); + } + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseSmokeTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseSmokeTest.java new file mode 100644 index 000000000..99be8f790 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseSmokeTest.java @@ -0,0 +1,265 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.core.identity.Contact; +import com.demcha.compose.document.templates.cv.data.CvDocument; +import com.demcha.compose.document.templates.cv.data.CvEntry; +import com.demcha.compose.document.templates.cv.data.CvIdentity; +import com.demcha.compose.document.templates.cv.data.CvName; +import com.demcha.compose.document.templates.cv.data.EntriesSection; +import com.demcha.compose.document.templates.cv.data.Slot; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Smoke test for {@link TealPulse} — proves the preset renders a + * {@link CvDocument} end-to-end with its packaged marks, sets its own page + * over whatever the caller configured, draws only the tagline's body, and + * carries the channels and the titles as link annotations, which move no pixel + * and no layout node so neither gate would notice them going missing. + * + *

It also pins what happens past one page, which is not what the other + * ported CV presets do: the bands flow, so a longer CV runs onto a second + * page, while the body row itself cannot be split.

+ */ +class TealPulseSmokeTest { + + private static byte[] render(CvDocument doc) throws Exception { + // The preset owns its page geometry, so the session starts unconfigured. + try (DocumentSession session = GraphCompose.document().create()) { + TealPulse.create().compose(session, doc); + assertThat(session.roots()).isNotEmpty(); + byte[] pdfBytes = session.toPdfBytes(); + assertThat(pdfBytes).isNotEmpty(); + return pdfBytes; + } + } + + private static String textOf(byte[] pdfBytes) throws Exception { + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + return new PDFTextStripper().getText(document); + } + } + + /** + * The text with its spaces removed. The sheet's headings and its name are + * letter-spaced with real space characters, so what is being asserted is + * the wording, not the tracking. + */ + private static String compact(String text) { + return text.replaceAll("(?U)\\s", ""); + } + + private static List linkTargets(byte[] pdfBytes) throws Exception { + List targets = new ArrayList<>(); + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + for (PDPage page : document.getPages()) { + for (PDAnnotation annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationLink link + && link.getAction() instanceof PDActionURI uri) { + targets.add(uri.getURI()); + } + } + } + } + return targets; + } + + /** The canonical document with one section replaced. */ + private static CvDocument withSection(Slot slot, EntriesSection replacement, + String replacedTitle) { + List placements = new ArrayList<>(); + for (CvDocument.Placement placement : TealPulseFixtures.canonicalCv().placements()) { + if (placement.section().title().equals(replacedTitle)) { + placements.add(new CvDocument.Placement(slot, replacement)); + } else { + placements.add(placement); + } + } + return new CvDocument(TealPulseFixtures.identity(), placements); + } + + @Test + void exposesStableIdentity() { + DocumentTemplate template = TealPulse.create(); + assertThat(template.id()).isEqualTo(TealPulse.ID); + assertThat(template.displayName()).isEqualTo(TealPulse.DISPLAY_NAME); + } + + @Test + void rendersCanonicalCvWithPackagedMarks() throws Exception { + render(TealPulseFixtures.canonicalCv()); + } + + @Test + void canonicalRenderCarriesEveryBandsText() throws Exception { + String text = textOf(render(TealPulseFixtures.canonicalCv())); + assertThat(text) + .contains("isabella.moore@email.com") + .contains("Patient Assessment") + .contains("Compassionate Registered Nurse") + .contains("Manchester Royal Infirmary") + .contains("BSc (Hons) Adult Nursing") + .contains("NMC Registered Nurse") + .contains("Right to Work:"); + assertThat(compact(text)) + .contains("ISABELLAMOORE") + .contains("REGISTEREDNURSE") + .contains("PROFESSIONALEXPERIENCE") + .contains("COMPASSIONATECARE.CLINICALEXCELLENCE.BETTEROUTCOMES."); + } + + @Test + void theTaglineBerthDrawsItsBodyAndNotItsTitle() throws Exception { + // The title is how a document names the berth; only the line reaches + // the sheet, so a document is free to call the berth what it likes. + String text = compact(textOf(render(TealPulseFixtures.canonicalCv()))); + assertThat(text) + .contains("COMPASSIONATECARE.") + .doesNotContain("TAGLINE"); + } + + @Test + void thePresetSetsItsOwnPageOverTheCallersOwn() throws Exception { + // The sheet is a fixed composition on the design's own page, so a + // caller who configures A4 still gets the design's proportion. + try (DocumentSession session = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .margin(40f, 40f, 40f, 40f) + .create()) { + TealPulse.create().compose(session, TealPulseFixtures.canonicalCv()); + byte[] pdfBytes = session.toPdfBytes(); + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + float height = document.getPage(0).getMediaBox().getHeight(); + assertThat(height).isCloseTo((float) TealPulseStyles.PAGE_HEIGHT, + org.assertj.core.data.Offset.offset(0.5f)); + assertThat(height).isNotCloseTo((float) DocumentPageSize.A4.height(), + org.assertj.core.data.Offset.offset(1.0f)); + } + } + } + + @Test + void contactChannelsAndLinksAreClickable() throws Exception { + List targets = linkTargets(render(TealPulseFixtures.canonicalCv())); + assertThat(targets) + .contains("mailto:isabella.moore@email.com") + .contains("tel:+447700900123") + .contains("https://www.linkedin.com/in/isabellamoore-rn"); + } + + @Test + void aLinkShowsItsLabelAndHidesItsAddress() throws Exception { + // The strip draws the label, so its gaps are the same whatever the + // profile behind it is called; the address is reachable, not written. + byte[] pdfBytes = render(TealPulseFixtures.canonicalCv()); + assertThat(textOf(pdfBytes)) + .contains("LinkedIn") + .doesNotContain("linkedin.com/in/isabellamoore-rn"); + assertThat(linkTargets(pdfBytes)) + .contains("https://www.linkedin.com/in/isabellamoore-rn"); + } + + @Test + void aRoleAndADegreeBecomeLinksWhenTheirEntryCarriesOne() throws Exception { + CvDocument linked = withSection(Slot.MAIN, + new EntriesSection("PROFESSIONAL EXPERIENCE", List.of( + CvEntry.builder("Senior Staff Nurse") + .subtitle("Manchester Royal Infirmary, Manchester, UK") + .date("2021") + .link("https://example.test/mri") + .body("Deliver direct nursing care.") + .build())), + "PROFESSIONAL EXPERIENCE"); + List placements = new ArrayList<>(); + for (CvDocument.Placement placement : linked.placements()) { + if (placement.section().title().equals("EDUCATION")) { + placements.add(new CvDocument.Placement(Slot.FOOTER, + new EntriesSection("EDUCATION", List.of( + CvEntry.builder("BSc (Hons) Adult Nursing") + .subtitle("University of Manchester") + .date("2013") + .link("https://example.test/manchester") + .build())))); + } else { + placements.add(placement); + } + } + + List targets = linkTargets( + render(new CvDocument(TealPulseFixtures.identity(), placements))); + assertThat(targets) + .contains("https://example.test/mri") + .contains("https://example.test/manchester"); + } + + @Test + void aDocumentWithNothingButAnIdentityStillRenders() throws Exception { + // Every berth empty: no badge, no heading, and nothing drawn but the + // mark, the name and the rules that frame them. + render(new CvDocument(TealPulseFixtures.identity(), List.of())); + } + + @Test + void anIdentityWithoutLinksDrawsTheThreeChannels() throws Exception { + CvIdentity noLinks = new CvIdentity(CvName.of("Ada", "Lovelace"), "Analyst", + new Contact("+44 20 7946 0000", "ada@example.test", "London, UK"), + List.of(), Optional.empty()); + + // A run that breaks at a space carries one annotation per piece, so it + // is the set of targets that is asserted, not the count. + byte[] pdfBytes = render(new CvDocument(noLinks, List.of())); + assertThat(Set.copyOf(linkTargets(pdfBytes))) + .containsExactlyInAnyOrder("mailto:ada@example.test", "tel:+442079460000"); + } + + @Test + void aLongerCvCarriesTheClosingBandOntoASecondPage() throws Exception { + // The bands are stacked in the page flow, so a CV with more roles than + // the design holds runs over rather than losing anything. + try (DocumentSession session = GraphCompose.document().create()) { + TealPulse.create().compose(session, withRoles(6)); + assertThat(session.layoutSnapshot().totalPages()).isEqualTo(2); + } + } + + @Test + void refusesABodyTallerThanOnePage() { + // What cannot flow is the body itself: its two columns are one row, + // and a row is atomic, so it raises rather than being cut in half. + assertThatThrownBy(() -> render(withRoles(14))) + .hasMessageContaining("BodyGrid"); + } + + /** The canonical CV with its experience replaced by {@code count} roles. */ + private static CvDocument withRoles(int count) { + List many = new ArrayList<>(); + for (int index = 0; index < count; index++) { + many.add(CvEntry.builder("Senior Staff Nurse " + index) + .subtitle("Manchester Royal Infirmary, Manchester, UK") + .date("2021") + .body("Deliver direct nursing care for adult patients on a busy ward.") + .build()); + } + return withSection(Slot.MAIN, + new EntriesSection("PROFESSIONAL EXPERIENCE", many), + "PROFESSIONAL EXPERIENCE"); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseVisualParityTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseVisualParityTest.java new file mode 100644 index 000000000..70dd5fd05 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TealPulseVisualParityTest.java @@ -0,0 +1,41 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.testing.visual.PdfVisualRegression; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; + +/** + * Pixel-diff visual parity gate for {@link TealPulse} — the canonical + * one-page CV against a checked-in baseline. + * + *

Re-blessing baselines — after a deliberate visual + * change, re-run with {@code -Dgraphcompose.visual.approve=true} and commit + * the updated PNG with the change. Baselines live under + * {@code src/test/resources/visual-baselines/cv-v2-layered/}.

+ */ +class TealPulseVisualParityTest { + + private static final Path BASELINE_ROOT = Path.of( + "src", "test", "resources", "visual-baselines", "cv-v2-layered"); + + private static final long PIXEL_DIFF_BUDGET = 50_000L; + private static final int PER_PIXEL_TOLERANCE = 8; + + @Test + void rendersWithinPixelDiffBudget() throws Exception { + byte[] pdfBytes; + try (DocumentSession document = GraphCompose.document().create()) { + TealPulse.create().compose(document, TealPulseFixtures.canonicalCv()); + pdfBytes = document.toPdfBytes(); + } + + PdfVisualRegression.standard() + .baselineRoot(BASELINE_ROOT) + .perPixelTolerance(PER_PIXEL_TOLERANCE) + .mismatchedPixelBudget(PIXEL_DIFF_BUDGET) + .assertMatchesBaseline("teal_pulse", pdfBytes); + } +} diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/teal_pulse_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/teal_pulse_layout.json new file mode 100644 index 000000000..5ec326d45 --- /dev/null +++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/teal_pulse_layout.json @@ -0,0 +1,3647 @@ +{ + "formatVersion" : "2.0", + "canvas" : { + "pageWidth" : 595.28, + "pageHeight" : 769.6, + "innerWidth" : 595.28, + "innerHeight" : 769.6, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, + "totalPages" : 1, + "nodes" : [ { + "path" : "TealPulseCv[0]", + "entityName" : "TealPulseCv", + "entityKind" : "ContainerNode", + "parentPath" : null, + "childIndex" : 0, + "depth" : 1, + "layer" : 1, + "computedX" : 0.0, + "computedY" : 2.0, + "placementX" : 0.0, + "placementY" : 2.0, + "placementWidth" : 595.28, + "placementHeight" : 767.601, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 595.28, + "contentHeight" : 767.601, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 18.35, + "right" : 28.604, + "bottom" : 7.556, + "left" : 28.604 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]", + "entityName" : "Masthead", + "entityKind" : "RowNode", + "parentPath" : "TealPulseCv[0]", + "childIndex" : 0, + "depth" : 2, + "layer" : 2, + "computedX" : 28.604, + "computedY" : 676.575, + "placementX" : 28.604, + "placementY" : 676.575, + "placementWidth" : 538.073, + "placementHeight" : 74.676, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 74.676, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 9.175, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]", + "entityName" : "BrandMark", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/Masthead[0]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 28.604, + "computedY" : 685.579, + "placementX" : 28.604, + "placementY" : 685.579, + "placementWidth" : 71.239, + "placementHeight" : 56.668, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 71.239, + "contentHeight" : 56.668, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]/Icon_brandheart[0]", + "entityName" : "Icon_brandheart", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 675.363, + "placementX" : 28.604, + "placementY" : 675.363, + "placementWidth" : 71.239, + "placementHeight" : 77.1, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 71.239, + "contentHeight" : 77.1, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]/Icon_brandheart[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]/Icon_brandheart[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 675.363, + "placementX" : 28.604, + "placementY" : 675.363, + "placementWidth" : 71.239, + "placementHeight" : 77.1, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 71.239, + "contentHeight" : 77.1, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]/Icon_brandheart[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]/Icon_brandheart[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 28.604, + "computedY" : 675.363, + "placementX" : 28.604, + "placementY" : 675.363, + "placementWidth" : 77.1, + "placementHeight" : 77.1, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 77.1, + "contentHeight" : 77.1, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]/BrandPulse[1]", + "entityName" : "BrandPulse", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/Masthead[0]/BrandMark[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 17.81, + "computedY" : 688.278, + "placementX" : 17.81, + "placementY" : 688.278, + "placementWidth" : 92.827, + "placementHeight" : 44.255, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 92.827, + "contentHeight" : 44.255, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]/Identity[1]", + "entityName" : "Identity", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/Masthead[0]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 120.891, + "computedY" : 676.575, + "placementX" : 120.891, + "placementY" : 676.575, + "placementWidth" : 340.363, + "placementHeight" : 74.676, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 340.363, + "contentHeight" : 74.676, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]/Identity[1]/DisplayName[0]", + "entityName" : "DisplayName", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/Masthead[0]/Identity[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 120.891, + "computedY" : 695.951, + "placementX" : 120.891, + "placementY" : 695.951, + "placementWidth" : 340.363, + "placementHeight" : 55.3, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 340.363, + "contentHeight" : 55.3, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Masthead[0]/Identity[1]/ProfessionalTitle[1]", + "entityName" : "ProfessionalTitle", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/Masthead[0]/Identity[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 120.891, + "computedY" : 676.575, + "placementX" : 120.891, + "placementY" : 676.575, + "placementWidth" : 205.712, + "placementHeight" : 19.376, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 205.712, + "contentHeight" : 19.376, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ContactStrip[1]", + "entityName" : "ContactStrip", + "entityKind" : "RowNode", + "parentPath" : "TealPulseCv[0]", + "childIndex" : 1, + "depth" : 2, + "layer" : 2, + "computedX" : 28.604, + "computedY" : 646.892, + "placementX" : 28.604, + "placementY" : 646.892, + "placementWidth" : 538.073, + "placementHeight" : 20.508, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 20.508, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 12.413, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ContactStrip[1]/Contact_0_location[0]", + "entityName" : "Contact_0_location", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ContactStrip[1]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 28.604, + "computedY" : 651.505, + "placementX" : 28.604, + "placementY" : 651.505, + "placementWidth" : 120.885, + "placementHeight" : 15.6, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 120.885, + "contentHeight" : 15.6, + "margin" : { + "top" : -4.318, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ContactStrip[1]/ContactSeparator_1[1]", + "entityName" : "ContactSeparator_1", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]/ContactStrip[1]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 178.536, + "computedY" : 646.892, + "placementX" : 178.536, + "placementY" : 646.892, + "placementWidth" : 1.0, + "placementHeight" : 20.508, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.0, + "contentHeight" : 20.508, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ContactStrip[1]/Contact_1_email[2]", + "entityName" : "Contact_1_email", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ContactStrip[1]", + "childIndex" : 2, + "depth" : 3, + "layer" : 3, + "computedX" : 208.584, + "computedY" : 651.205, + "placementX" : 208.584, + "placementY" : 651.205, + "placementWidth" : 111.22, + "placementHeight" : 16.2, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 111.22, + "contentHeight" : 16.2, + "margin" : { + "top" : -4.318, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ContactStrip[1]/ContactSeparator_2[3]", + "entityName" : "ContactSeparator_2", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]/ContactStrip[1]", + "childIndex" : 3, + "depth" : 3, + "layer" : 3, + "computedX" : 348.852, + "computedY" : 646.892, + "placementX" : 348.852, + "placementY" : 646.892, + "placementWidth" : 1.0, + "placementHeight" : 20.508, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.0, + "contentHeight" : 20.508, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ContactStrip[1]/Contact_2_phone[4]", + "entityName" : "Contact_2_phone", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ContactStrip[1]", + "childIndex" : 4, + "depth" : 3, + "layer" : 3, + "computedX" : 378.9, + "computedY" : 652.155, + "placementX" : 378.9, + "placementY" : 652.155, + "placementWidth" : 81.613, + "placementHeight" : 14.3, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 81.613, + "contentHeight" : 14.3, + "margin" : { + "top" : -4.318, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ContactStrip[1]/ContactSeparator_3[5]", + "entityName" : "ContactSeparator_3", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]/ContactStrip[1]", + "childIndex" : 5, + "depth" : 3, + "layer" : 3, + "computedX" : 489.561, + "computedY" : 646.892, + "placementX" : 489.561, + "placementY" : 646.892, + "placementWidth" : 1.0, + "placementHeight" : 20.508, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.0, + "contentHeight" : 20.508, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ContactStrip[1]/Contact_3_linkedin[6]", + "entityName" : "Contact_3_linkedin", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ContactStrip[1]", + "childIndex" : 6, + "depth" : 3, + "layer" : 3, + "computedX" : 519.608, + "computedY" : 652.505, + "placementX" : 519.608, + "placementY" : 652.505, + "placementWidth" : 47.068, + "placementHeight" : 13.6, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 47.068, + "contentHeight" : 13.6, + "margin" : { + "top" : -4.318, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/MastheadRule[2]", + "entityName" : "MastheadRule", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]", + "childIndex" : 2, + "depth" : 2, + "layer" : 2, + "computedX" : 28.604, + "computedY" : 633.13, + "placementX" : 28.604, + "placementY" : 633.13, + "placementWidth" : 538.073, + "placementHeight" : 1.349, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 1.349, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 15.651, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]", + "entityName" : "BodyGrid", + "entityKind" : "RowNode", + "parentPath" : "TealPulseCv[0]", + "childIndex" : 3, + "depth" : 2, + "layer" : 2, + "computedX" : 28.604, + "computedY" : 177.009, + "placementX" : 28.604, + "placementY" : 177.009, + "placementWidth" : 538.073, + "placementHeight" : 440.47, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 440.47, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 16.191, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]", + "entityName" : "Sidebar", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 28.604, + "computedY" : 239.229, + "placementX" : 28.604, + "placementY" : 239.229, + "placementWidth" : 118.781, + "placementHeight" : 378.25, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 118.781, + "contentHeight" : 378.25, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 4.857, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetenciesDash[0]", + "entityName" : "CompetenciesDash", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 611.002, + "placementX" : 28.604, + "placementY" : 611.002, + "placementWidth" : 29.143, + "placementHeight" : 1.619, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 29.143, + "contentHeight" : 1.619, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.556, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetenciesHeading[1]", + "entityName" : "CompetenciesHeading", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 590.967, + "placementX" : 28.604, + "placementY" : 590.967, + "placementWidth" : 110.685, + "placementHeight" : 12.48, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 110.685, + "contentHeight" : 12.48, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 18.889, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "entityName" : "CompetencyItems", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 267.832, + "placementX" : 28.604, + "placementY" : 267.832, + "placementWidth" : 118.781, + "placementHeight" : 304.245, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 118.781, + "contentHeight" : 304.245, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_PatientAssessment[0]", + "entityName" : "Competency_PatientAssessment", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 561.169, + "placementX" : 28.604, + "placementY" : 561.169, + "placementWidth" : 90.748, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 90.748, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_CarePlanning[1]", + "entityName" : "Competency_CarePlanning", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 534.502, + "placementX" : 28.604, + "placementY" : 534.502, + "placementWidth" : 67.823, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 67.823, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_MedicationAdministration[2]", + "entityName" : "Competency_MedicationAdministration", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 507.835, + "placementX" : 28.604, + "placementY" : 507.835, + "placementWidth" : 118.781, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 118.781, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_ClinicalDocumentation[3]", + "entityName" : "Competency_ClinicalDocumentation", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 3, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 481.168, + "placementX" : 28.604, + "placementY" : 481.168, + "placementWidth" : 105.828, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 105.828, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_InfectionPrevention[4]", + "entityName" : "Competency_InfectionPrevention", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 4, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 454.501, + "placementX" : 28.604, + "placementY" : 454.501, + "placementWidth" : 94.302, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 94.302, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_IVTherapy[5]", + "entityName" : "Competency_IVTherapy", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 5, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 427.834, + "placementX" : 28.604, + "placementY" : 427.834, + "placementWidth" : 56.433, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 56.433, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_WoundCare[6]", + "entityName" : "Competency_WoundCare", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 6, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 401.167, + "placementX" : 28.604, + "placementY" : 401.167, + "placementWidth" : 63.378, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 63.378, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_PatientEducation[7]", + "entityName" : "Competency_PatientEducation", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 7, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 374.5, + "placementX" : 28.604, + "placementY" : 374.5, + "placementWidth" : 83.385, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 83.385, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_Safeguarding[8]", + "entityName" : "Competency_Safeguarding", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 8, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 347.833, + "placementX" : 28.604, + "placementY" : 347.833, + "placementWidth" : 64.687, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 64.687, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_TeamCollaboration[9]", + "entityName" : "Competency_TeamCollaboration", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 9, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 321.166, + "placementX" : 28.604, + "placementY" : 321.166, + "placementWidth" : 90.82, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 90.82, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_DischargeCoordination[10]", + "entityName" : "Competency_DischargeCoordination", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 10, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 294.499, + "placementX" : 28.604, + "placementY" : 294.499, + "placementWidth" : 106.901, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 106.901, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]/Competency_ElectronicHealthRecords[11]", + "entityName" : "Competency_ElectronicHealthRecords", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetencyItems[2]", + "childIndex" : 11, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 267.832, + "placementX" : 28.604, + "placementY" : 267.832, + "placementWidth" : 116.145, + "placementHeight" : 10.908, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 116.145, + "contentHeight" : 10.908, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]/CompetenciesClosingRule[3]", + "entityName" : "CompetenciesClosingRule", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Sidebar[0]", + "childIndex" : 3, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 239.229, + "placementX" : 28.604, + "placementY" : 239.229, + "placementWidth" : 116.573, + "placementHeight" : 1.619, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 116.573, + "contentHeight" : 1.619, + "margin" : { + "top" : 26.985, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "entityName" : "Main", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 171.37, + "computedY" : 177.009, + "placementX" : 171.37, + "placementY" : 177.009, + "placementWidth" : 395.306, + "placementHeight" : 440.47, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 395.306, + "contentHeight" : 440.47, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 17.27 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]", + "entityName" : "SectionHeader_PROFESSIONALSUMMARY", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 586.177, + "placementX" : 188.641, + "placementY" : 586.177, + "placementWidth" : 378.036, + "placementHeight" : 31.302, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 378.036, + "contentHeight" : 31.302, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 10.794, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/Badge_PROFESSIONALSUMMARY[0]", + "entityName" : "Badge_PROFESSIONALSUMMARY", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 586.177, + "placementX" : 188.641, + "placementY" : 586.177, + "placementWidth" : 31.302, + "placementHeight" : 31.302, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 31.302, + "contentHeight" : 31.302, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/Badge_PROFESSIONALSUMMARY[0]/Icon_summary[0]", + "entityName" : "Icon_summary", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/Badge_PROFESSIONALSUMMARY[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 194.292, + "computedY" : 591.828, + "placementX" : 194.292, + "placementY" : 591.828, + "placementWidth" : 20.0, + "placementHeight" : 20.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.0, + "contentHeight" : 20.0, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/Badge_PROFESSIONALSUMMARY[0]/Icon_summary[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/Badge_PROFESSIONALSUMMARY[0]/Icon_summary[0]", + "childIndex" : 0, + "depth" : 7, + "layer" : 7, + "computedX" : 194.292, + "computedY" : 591.828, + "placementX" : 194.292, + "placementY" : 591.828, + "placementWidth" : 20.0, + "placementHeight" : 20.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.0, + "contentHeight" : 20.0, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/Badge_PROFESSIONALSUMMARY[0]/Icon_summary[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/Badge_PROFESSIONALSUMMARY[0]/Icon_summary[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 8, + "layer" : 8, + "computedX" : 194.292, + "computedY" : 591.828, + "placementX" : 194.292, + "placementY" : 591.828, + "placementWidth" : 20.0, + "placementHeight" : 20.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.0, + "contentHeight" : 20.0, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/SectionHeaderRule_PROFESSIONALSUMMARY[1]", + "entityName" : "SectionHeaderRule_PROFESSIONALSUMMARY", + "entityKind" : "ShapeNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 229.118, + "computedY" : 601.153, + "placementX" : 229.118, + "placementY" : 601.153, + "placementWidth" : 337.559, + "placementHeight" : 1.349, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 337.559, + "contentHeight" : 1.349, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]/SectionHeading_PROFESSIONALSUMMARY[2]", + "entityName" : "SectionHeading_PROFESSIONALSUMMARY", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALSUMMARY[0]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 229.118, + "computedY" : 594.208, + "placementX" : 229.118, + "placementY" : 594.208, + "placementWidth" : 144.93, + "placementHeight" : 15.24, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 144.93, + "contentHeight" : 15.24, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SummaryText[1]", + "entityName" : "SummaryText", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 477.181, + "placementX" : 188.641, + "placementY" : 477.181, + "placementWidth" : 317.12, + "placementHeight" : 98.202, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 317.12, + "contentHeight" : 98.202, + "margin" : { + "top" : 0.0, + "right" : 56.668, + "bottom" : 15.651, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]", + "entityName" : "SectionHeader_PROFESSIONALEXPERIENCE", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 430.228, + "placementX" : 188.641, + "placementY" : 430.228, + "placementWidth" : 378.036, + "placementHeight" : 31.302, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 378.036, + "contentHeight" : 31.302, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.556, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]", + "entityName" : "Badge_PROFESSIONALEXPERIENCE", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 430.228, + "placementX" : 188.641, + "placementY" : 430.228, + "placementWidth" : 31.302, + "placementHeight" : 31.302, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 31.302, + "contentHeight" : 31.302, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]/Icon_experience[0]", + "entityName" : "Icon_experience", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 194.292, + "computedY" : 435.879, + "placementX" : 194.292, + "placementY" : 435.879, + "placementWidth" : 20.0, + "placementHeight" : 20.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.0, + "contentHeight" : 20.0, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]/Icon_experience[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]/Icon_experience[0]", + "childIndex" : 0, + "depth" : 7, + "layer" : 7, + "computedX" : 194.292, + "computedY" : 435.879, + "placementX" : 194.292, + "placementY" : 435.879, + "placementWidth" : 20.0, + "placementHeight" : 20.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.0, + "contentHeight" : 20.0, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]/Icon_experience[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]/Icon_experience[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 8, + "layer" : 8, + "computedX" : 194.292, + "computedY" : 435.879, + "placementX" : 194.292, + "placementY" : 435.879, + "placementWidth" : 20.0, + "placementHeight" : 20.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.0, + "contentHeight" : 20.0, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]/Icon_experience[0]/SvgIcon[0]/SvgLayer1[1]", + "entityName" : "SvgLayer1", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/Badge_PROFESSIONALEXPERIENCE[0]/Icon_experience[0]/SvgIcon[0]", + "childIndex" : 1, + "depth" : 8, + "layer" : 8, + "computedX" : 194.292, + "computedY" : 435.879, + "placementX" : 194.292, + "placementY" : 435.879, + "placementWidth" : 20.0, + "placementHeight" : 20.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.0, + "contentHeight" : 20.0, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/SectionHeaderRule_PROFESSIONALEXPERIENCE[1]", + "entityName" : "SectionHeaderRule_PROFESSIONALEXPERIENCE", + "entityKind" : "ShapeNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 229.118, + "computedY" : 445.204, + "placementX" : 229.118, + "placementY" : 445.204, + "placementWidth" : 337.559, + "placementHeight" : 1.349, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 337.559, + "contentHeight" : 1.349, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]/SectionHeading_PROFESSIONALEXPERIENCE[2]", + "entityName" : "SectionHeading_PROFESSIONALEXPERIENCE", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/SectionHeader_PROFESSIONALEXPERIENCE[2]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 229.118, + "computedY" : 438.259, + "placementX" : 229.118, + "placementY" : 438.259, + "placementWidth" : 160.457, + "placementHeight" : 15.24, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 160.457, + "contentHeight" : 15.24, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHeadline_0[3]", + "entityName" : "ExperienceHeadline_0", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 3, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 411.992, + "placementX" : 188.641, + "placementY" : 411.992, + "placementWidth" : 325.998, + "placementHeight" : 10.68, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 325.998, + "contentHeight" : 10.68, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.016, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]", + "entityName" : "ExperienceHighlights_0", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 4, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 338.962, + "placementX" : 188.641, + "placementY" : 338.962, + "placementWidth" : 321.805, + "placementHeight" : 66.014, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 321.805, + "contentHeight" : 66.014, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 18.35, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]/Highlight_0_Deliverdirectnursingcareforadultpatientsonabusyacutemedicalward[0]", + "entityName" : "Highlight_0_Deliverdirectnursingcareforadultpatientsonabusyacutemedicalward", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 395.184, + "placementX" : 188.641, + "placementY" : 395.184, + "placementWidth" : 279.348, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 279.348, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]/Highlight_0_Coordinatecareplanswithdoctorstherapistsandsupportstafftoimproveoutcomes[1]", + "entityName" : "Highlight_0_Coordinatecareplanswithdoctorstherapistsandsupportstafftoimproveoutcomes", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 381.128, + "placementX" : 188.641, + "placementY" : 381.128, + "placementWidth" : 321.805, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 321.805, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]/Highlight_0_Supervisejuniornursesandsupportstudentplacementsduringclinicalrotations[2]", + "entityName" : "Highlight_0_Supervisejuniornursesandsupportstudentplacementsduringclinicalrotations", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 367.073, + "placementX" : 188.641, + "placementY" : 367.073, + "placementWidth" : 299.585, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 299.585, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]/Highlight_0_Improveddischargecoordinationandpatientcommunicationacrosstheward[3]", + "entityName" : "Highlight_0_Improveddischargecoordinationandpatientcommunicationacrosstheward", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]", + "childIndex" : 3, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 353.017, + "placementX" : 188.641, + "placementY" : 353.017, + "placementWidth" : 289.874, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 289.874, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]/Highlight_0_Maintainprecisedocumentationandensurecompliancewithclinicalstandards[4]", + "entityName" : "Highlight_0_Maintainprecisedocumentationandensurecompliancewithclinicalstandards", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_0[4]", + "childIndex" : 4, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 338.962, + "placementX" : 188.641, + "placementY" : 338.962, + "placementWidth" : 294.444, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 294.444, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHeadline_1[5]", + "entityName" : "ExperienceHeadline_1", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 5, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 309.932, + "placementX" : 188.641, + "placementY" : 309.932, + "placementWidth" : 256.133, + "placementHeight" : 10.68, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 256.133, + "contentHeight" : 10.68, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.016, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]", + "entityName" : "ExperienceHighlights_1", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 6, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 250.957, + "placementX" : 188.641, + "placementY" : 250.957, + "placementWidth" : 351.719, + "placementHeight" : 51.959, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 351.719, + "contentHeight" : 51.959, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 18.35, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]/Highlight_1_Providednursingcareacrossmedicalandsurgicalunits[0]", + "entityName" : "Highlight_1_Providednursingcareacrossmedicalandsurgicalunits", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 293.124, + "placementX" : 188.641, + "placementY" : 293.124, + "placementWidth" : 209.76, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 209.76, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]/Highlight_1_Administeredmedicationsmonitoredvitalsignsandescalateddeterioratingpatientspromptly[1]", + "entityName" : "Highlight_1_Administeredmedicationsmonitoredvitalsignsandescalateddeterioratingpatientspromptly", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 279.069, + "placementX" : 188.641, + "placementY" : 279.069, + "placementWidth" : 351.719, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 351.719, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]/Highlight_1_Supportedinfectioncontrolproceduresandsafedischargeplanning[2]", + "entityName" : "Highlight_1_Supportedinfectioncontrolproceduresandsafedischargeplanning", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 265.013, + "placementX" : 188.641, + "placementY" : 265.013, + "placementWidth" : 255.643, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 255.643, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]/Highlight_1_Builtstrongrapportwithpatientsandfamiliesduringtreatmentandrecovery[3]", + "entityName" : "Highlight_1_Builtstrongrapportwithpatientsandfamiliesduringtreatmentandrecovery", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_1[6]", + "childIndex" : 3, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 250.957, + "placementX" : 188.641, + "placementY" : 250.957, + "placementWidth" : 289.05, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 289.05, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHeadline_2[7]", + "entityName" : "ExperienceHeadline_2", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 7, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 221.928, + "placementX" : 188.641, + "placementY" : 221.928, + "placementWidth" : 357.282, + "placementHeight" : 10.68, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 357.282, + "contentHeight" : 10.68, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.016, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_2[8]", + "entityName" : "ExperienceHighlights_2", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]", + "childIndex" : 8, + "depth" : 4, + "layer" : 4, + "computedX" : 188.641, + "computedY" : 177.009, + "placementX" : 188.641, + "placementY" : 177.009, + "placementWidth" : 321.315, + "placementHeight" : 37.903, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 321.315, + "contentHeight" : 37.903, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_2[8]/Highlight_2_Deliveredhomebasednursingcareandpatienteducation[0]", + "entityName" : "Highlight_2_Deliveredhomebasednursingcareandpatienteducation", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_2[8]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 205.12, + "placementX" : 188.641, + "placementY" : 205.12, + "placementWidth" : 220.645, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 220.645, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_2[8]/Highlight_2_Managedwoundcaremedicationsupportandfollowupvisitsforvulnerablepatients[1]", + "entityName" : "Highlight_2_Managedwoundcaremedicationsupportandfollowupvisitsforvulnerablepatients", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_2[8]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 191.064, + "placementX" : 188.641, + "placementY" : 191.064, + "placementWidth" : 321.315, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 321.315, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_2[8]/Highlight_2_WorkedcloselywithGPssocialservicesandfamiliestocoordinateholisticcare[2]", + "entityName" : "Highlight_2_WorkedcloselywithGPssocialservicesandfamiliestocoordinateholisticcare", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/BodyGrid[3]/Main[1]/ExperienceHighlights_2[8]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 188.641, + "computedY" : 177.009, + "placementX" : 188.641, + "placementY" : 177.009, + "placementWidth" : 299.862, + "placementHeight" : 9.792, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 299.862, + "contentHeight" : 9.792, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBandRule[4]", + "entityName" : "ClosingBandRule", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]", + "childIndex" : 4, + "depth" : 2, + "layer" : 2, + "computedX" : 28.604, + "computedY" : 159.469, + "placementX" : 28.604, + "placementY" : 159.469, + "placementWidth" : 538.073, + "placementHeight" : 1.349, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 1.349, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 14.572, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]", + "entityName" : "ClosingBand", + "entityKind" : "RowNode", + "parentPath" : "TealPulseCv[0]", + "childIndex" : 5, + "depth" : 2, + "layer" : 2, + "computedX" : 28.604, + "computedY" : 43.334, + "placementX" : 28.604, + "placementY" : 43.334, + "placementWidth" : 538.073, + "placementHeight" : 101.564, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 101.564, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 11.873, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]", + "entityName" : "EducationColumn", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 28.604, + "computedY" : 60.866, + "placementX" : 28.604, + "placementY" : 60.866, + "placementWidth" : 121.179, + "placementHeight" : 84.031, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 121.179, + "contentHeight" : 84.031, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]", + "entityName" : "ColumnHeader_EDUCATION", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 117.373, + "placementX" : 28.604, + "placementY" : 117.373, + "placementWidth" : 121.179, + "placementHeight" : 27.524, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 121.179, + "contentHeight" : 27.524, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 11.873, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/Badge_EDUCATION[0]", + "entityName" : "Badge_EDUCATION", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 28.604, + "computedY" : 117.373, + "placementX" : 28.604, + "placementY" : 117.373, + "placementWidth" : 27.524, + "placementHeight" : 27.524, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 27.524, + "contentHeight" : 27.524, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/Badge_EDUCATION[0]/Icon_education[0]", + "entityName" : "Icon_education", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/Badge_EDUCATION[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 33.616, + "computedY" : 122.385, + "placementX" : 33.616, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/Badge_EDUCATION[0]/Icon_education[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/Badge_EDUCATION[0]/Icon_education[0]", + "childIndex" : 0, + "depth" : 7, + "layer" : 7, + "computedX" : 33.616, + "computedY" : 122.385, + "placementX" : 33.616, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/Badge_EDUCATION[0]/Icon_education[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/Badge_EDUCATION[0]/Icon_education[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 8, + "layer" : 8, + "computedX" : 33.616, + "computedY" : 122.385, + "placementX" : 33.616, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/ColumnHeadingStack_EDUCATION[1]", + "entityName" : "ColumnHeadingStack_EDUCATION", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 65.842, + "computedY" : 115.526, + "placementX" : 65.842, + "placementY" : 115.526, + "placementWidth" : 83.94, + "placementHeight" : 18.266, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 83.94, + "contentHeight" : 18.266, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/ColumnHeadingStack_EDUCATION[1]/ColumnHeading_EDUCATION[0]", + "entityName" : "ColumnHeading_EDUCATION", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/ColumnHeadingStack_EDUCATION[1]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 65.842, + "computedY" : 122.272, + "placementX" : 65.842, + "placementY" : 122.272, + "placementWidth" : 51.84, + "placementHeight" : 11.52, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 51.84, + "contentHeight" : 11.52, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/ColumnHeadingStack_EDUCATION[1]/ColumnHeadingRule_EDUCATION[1]", + "entityName" : "ColumnHeadingRule_EDUCATION", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/ColumnHeader_EDUCATION[0]/ColumnHeadingStack_EDUCATION[1]", + "childIndex" : 1, + "depth" : 6, + "layer" : 6, + "computedX" : 65.842, + "computedY" : 115.526, + "placementX" : 65.842, + "placementY" : 115.526, + "placementWidth" : 83.94, + "placementHeight" : 1.619, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 83.94, + "contentHeight" : 1.619, + "margin" : { + "top" : 5.127, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/EducationDegree_0[1]", + "entityName" : "EducationDegree_0", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 92.84, + "placementX" : 28.604, + "placementY" : 92.84, + "placementWidth" : 118.804, + "placementHeight" : 12.66, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 118.804, + "contentHeight" : 12.66, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 5.667, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/EducationInstitution_0[2]", + "entityName" : "EducationInstitution_0", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 76.853, + "placementX" : 28.604, + "placementY" : 76.853, + "placementWidth" : 95.907, + "placementHeight" : 10.32, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 95.907, + "contentHeight" : 10.32, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 5.667, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]/EducationDates_0[3]", + "entityName" : "EducationDates_0", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/EducationColumn[0]", + "childIndex" : 3, + "depth" : 4, + "layer" : 4, + "computedX" : 28.604, + "computedY" : 60.866, + "placementX" : 28.604, + "placementY" : 60.866, + "placementWidth" : 44.892, + "placementHeight" : 10.32, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 44.892, + "contentHeight" : 10.32, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]", + "entityName" : "CertificationsColumn", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 171.37, + "computedY" : 43.334, + "placementX" : 171.37, + "placementY" : 43.334, + "placementWidth" : 198.167, + "placementHeight" : 101.564, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 198.167, + "contentHeight" : 101.564, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 19.429 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]", + "entityName" : "ColumnHeader_CERTIFICATIONS", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 190.799, + "computedY" : 117.373, + "placementX" : 190.799, + "placementY" : 117.373, + "placementWidth" : 162.316, + "placementHeight" : 27.524, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 162.316, + "contentHeight" : 27.524, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 11.873, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]", + "entityName" : "Badge_CERTIFICATIONS", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 190.799, + "computedY" : 117.373, + "placementX" : 190.799, + "placementY" : 117.373, + "placementWidth" : 27.524, + "placementHeight" : 27.524, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 27.524, + "contentHeight" : 27.524, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]/Icon_certifications[0]", + "entityName" : "Icon_certifications", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 195.812, + "computedY" : 122.385, + "placementX" : 195.812, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]/Icon_certifications[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]/Icon_certifications[0]", + "childIndex" : 0, + "depth" : 7, + "layer" : 7, + "computedX" : 195.812, + "computedY" : 122.385, + "placementX" : 195.812, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]/Icon_certifications[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]/Icon_certifications[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 8, + "layer" : 8, + "computedX" : 195.812, + "computedY" : 122.385, + "placementX" : 195.812, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]/Icon_certifications[0]/SvgIcon[0]/SvgLayer1[1]", + "entityName" : "SvgLayer1", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/Badge_CERTIFICATIONS[0]/Icon_certifications[0]/SvgIcon[0]", + "childIndex" : 1, + "depth" : 8, + "layer" : 8, + "computedX" : 195.812, + "computedY" : 122.385, + "placementX" : 195.812, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/ColumnHeadingStack_CERTIFICATIONS[1]", + "entityName" : "ColumnHeadingStack_CERTIFICATIONS", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 228.038, + "computedY" : 115.526, + "placementX" : 228.038, + "placementY" : 115.526, + "placementWidth" : 125.077, + "placementHeight" : 18.266, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 125.077, + "contentHeight" : 18.266, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/ColumnHeadingStack_CERTIFICATIONS[1]/ColumnHeading_CERTIFICATIONS[0]", + "entityName" : "ColumnHeading_CERTIFICATIONS", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/ColumnHeadingStack_CERTIFICATIONS[1]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 228.038, + "computedY" : 122.272, + "placementX" : 228.038, + "placementY" : 122.272, + "placementWidth" : 77.117, + "placementHeight" : 11.52, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 77.117, + "contentHeight" : 11.52, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/ColumnHeadingStack_CERTIFICATIONS[1]/ColumnHeadingRule_CERTIFICATIONS[1]", + "entityName" : "ColumnHeadingRule_CERTIFICATIONS", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/ColumnHeader_CERTIFICATIONS[0]/ColumnHeadingStack_CERTIFICATIONS[1]", + "childIndex" : 1, + "depth" : 6, + "layer" : 6, + "computedX" : 228.038, + "computedY" : 115.526, + "placementX" : 228.038, + "placementY" : 115.526, + "placementWidth" : 125.077, + "placementHeight" : 1.619, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 125.077, + "contentHeight" : 1.619, + "margin" : { + "top" : 5.127, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]", + "entityName" : "CertificationItems", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 190.799, + "computedY" : 43.334, + "placementX" : 190.799, + "placementY" : 43.334, + "placementWidth" : 178.738, + "placementHeight" : 62.166, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 178.738, + "contentHeight" : 62.166, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]/Certification_NMCRegisteredNurse[0]", + "entityName" : "Certification_NMCRegisteredNurse", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 190.799, + "computedY" : 95.18, + "placementX" : 190.799, + "placementY" : 95.18, + "placementWidth" : 99.825, + "placementHeight" : 10.32, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 99.825, + "contentHeight" : 10.32, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]/Certification_ImmediateLifeSupportILS[1]", + "entityName" : "Certification_ImmediateLifeSupportILS", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 190.799, + "computedY" : 77.898, + "placementX" : 190.799, + "placementY" : 77.898, + "placementWidth" : 118.882, + "placementHeight" : 10.32, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 118.882, + "contentHeight" : 10.32, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]/Certification_VenepunctureandCannulationCertification[2]", + "entityName" : "Certification_VenepunctureandCannulationCertification", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 190.799, + "computedY" : 60.616, + "placementX" : 190.799, + "placementY" : 60.616, + "placementWidth" : 178.738, + "placementHeight" : 10.32, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 178.738, + "contentHeight" : 10.32, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]/Certification_SafeguardingAdultsLevel3[3]", + "entityName" : "Certification_SafeguardingAdultsLevel3", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/CertificationsColumn[1]/CertificationItems[1]", + "childIndex" : 3, + "depth" : 5, + "layer" : 5, + "computedX" : 190.799, + "computedY" : 43.334, + "placementX" : 190.799, + "placementY" : 43.334, + "placementWidth" : 117.214, + "placementHeight" : 10.32, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 117.214, + "contentHeight" : 10.32, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]", + "entityName" : "AdditionalColumn", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]", + "childIndex" : 2, + "depth" : 3, + "layer" : 3, + "computedX" : 374.703, + "computedY" : 43.751, + "placementX" : 374.703, + "placementY" : 43.751, + "placementWidth" : 191.974, + "placementHeight" : 101.146, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 191.974, + "contentHeight" : 101.146, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 19.429 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]", + "entityName" : "ColumnHeader_ADDITIONALINFORMATION", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 394.132, + "computedY" : 117.373, + "placementX" : 394.132, + "placementY" : 117.373, + "placementWidth" : 172.545, + "placementHeight" : 27.524, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 172.545, + "contentHeight" : 27.524, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 11.873, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]", + "entityName" : "Badge_ADDITIONALINFORMATION", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 394.132, + "computedY" : 117.373, + "placementX" : 394.132, + "placementY" : 117.373, + "placementWidth" : 27.524, + "placementHeight" : 27.524, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 27.524, + "contentHeight" : 27.524, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]/Icon_additional[0]", + "entityName" : "Icon_additional", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 399.144, + "computedY" : 122.385, + "placementX" : 399.144, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]/Icon_additional[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]/Icon_additional[0]", + "childIndex" : 0, + "depth" : 7, + "layer" : 7, + "computedX" : 399.144, + "computedY" : 122.385, + "placementX" : 399.144, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]/Icon_additional[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]/Icon_additional[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 8, + "layer" : 8, + "computedX" : 399.144, + "computedY" : 122.385, + "placementX" : 399.144, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]/Icon_additional[0]/SvgIcon[0]/SvgLayer1[1]", + "entityName" : "SvgLayer1", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/Badge_ADDITIONALINFORMATION[0]/Icon_additional[0]/SvgIcon[0]", + "childIndex" : 1, + "depth" : 8, + "layer" : 8, + "computedX" : 399.144, + "computedY" : 122.385, + "placementX" : 399.144, + "placementY" : 122.385, + "placementWidth" : 17.5, + "placementHeight" : 17.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 17.5, + "contentHeight" : 17.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/ColumnHeadingStack_ADDITIONALINFORMATION[1]", + "entityName" : "ColumnHeadingStack_ADDITIONALINFORMATION", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 431.37, + "computedY" : 115.526, + "placementX" : 431.37, + "placementY" : 115.526, + "placementWidth" : 135.306, + "placementHeight" : 18.266, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 135.306, + "contentHeight" : 18.266, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/ColumnHeadingStack_ADDITIONALINFORMATION[1]/ColumnHeading_ADDITIONALINFORMATION[0]", + "entityName" : "ColumnHeading_ADDITIONALINFORMATION", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/ColumnHeadingStack_ADDITIONALINFORMATION[1]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 431.37, + "computedY" : 122.272, + "placementX" : 431.37, + "placementY" : 122.272, + "placementWidth" : 123.773, + "placementHeight" : 11.52, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 123.773, + "contentHeight" : 11.52, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/ColumnHeadingStack_ADDITIONALINFORMATION[1]/ColumnHeadingRule_ADDITIONALINFORMATION[1]", + "entityName" : "ColumnHeadingRule_ADDITIONALINFORMATION", + "entityKind" : "LineNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/ColumnHeader_ADDITIONALINFORMATION[0]/ColumnHeadingStack_ADDITIONALINFORMATION[1]", + "childIndex" : 1, + "depth" : 6, + "layer" : 6, + "computedX" : 431.37, + "computedY" : 115.526, + "placementX" : 431.37, + "placementY" : 115.526, + "placementWidth" : 135.306, + "placementHeight" : 1.619, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 135.306, + "contentHeight" : 1.619, + "margin" : { + "top" : 5.127, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/AdditionalFacts[1]", + "entityName" : "AdditionalFacts", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 394.132, + "computedY" : 43.751, + "placementX" : 394.132, + "placementY" : 43.751, + "placementWidth" : 163.122, + "placementHeight" : 61.749, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 163.122, + "contentHeight" : 61.749, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 42.636, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/AdditionalFacts[1]/Fact_Languages[0]", + "entityName" : "Fact_Languages", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/AdditionalFacts[1]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 394.132, + "computedY" : 80.15, + "placementX" : 394.132, + "placementY" : 80.15, + "placementWidth" : 105.367, + "placementHeight" : 25.35, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 105.367, + "contentHeight" : 25.35, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/AdditionalFacts[1]/Fact_RighttoWork[1]", + "entityName" : "Fact_RighttoWork", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/AdditionalFacts[1]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 394.132, + "computedY" : 61.95, + "placementX" : 394.132, + "placementY" : 61.95, + "placementWidth" : 120.486, + "placementHeight" : 10.32, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 120.486, + "contentHeight" : 10.32, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/AdditionalFacts[1]/Fact_Availability[2]", + "entityName" : "Fact_Availability", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]/ClosingBand[5]/AdditionalColumn[2]/AdditionalFacts[1]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 394.132, + "computedY" : 43.751, + "placementX" : 394.132, + "placementY" : 43.751, + "placementWidth" : 104.645, + "placementHeight" : 10.32, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 104.645, + "contentHeight" : 10.32, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/TaglineRule[6]", + "entityName" : "TaglineRule", + "entityKind" : "ShapeContainerNode", + "parentPath" : "TealPulseCv[0]", + "childIndex" : 6, + "depth" : 2, + "layer" : 2, + "computedX" : 28.604, + "computedY" : 20.127, + "placementX" : 28.604, + "placementY" : 20.127, + "placementWidth" : 538.073, + "placementHeight" : 11.334, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 11.334, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.864, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/TaglineRule[6]/TaglineRuleLine[0]", + "entityName" : "TaglineRuleLine", + "entityKind" : "ShapeNode", + "parentPath" : "TealPulseCv[0]/TaglineRule[6]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 28.604, + "computedY" : 25.119, + "placementX" : 28.604, + "placementY" : 25.119, + "placementWidth" : 538.073, + "placementHeight" : 1.349, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 1.349, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/TaglineRule[6]/Icon_heart[1]", + "entityName" : "Icon_heart", + "entityKind" : "SectionNode", + "parentPath" : "TealPulseCv[0]/TaglineRule[6]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 551.776, + "computedY" : 18.344, + "placementX" : 551.776, + "placementY" : 18.344, + "placementWidth" : 14.9, + "placementHeight" : 14.9, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 14.9, + "contentHeight" : 14.9, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/TaglineRule[6]/Icon_heart[1]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "TealPulseCv[0]/TaglineRule[6]/Icon_heart[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 551.776, + "computedY" : 18.344, + "placementX" : 551.776, + "placementY" : 18.344, + "placementWidth" : 14.9, + "placementHeight" : 14.9, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 14.9, + "contentHeight" : 14.9, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/TaglineRule[6]/Icon_heart[1]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "TealPulseCv[0]/TaglineRule[6]/Icon_heart[1]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 551.776, + "computedY" : 18.344, + "placementX" : 551.776, + "placementY" : 18.344, + "placementWidth" : 14.9, + "placementHeight" : 14.9, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 14.9, + "contentHeight" : 14.9, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "TealPulseCv[0]/Tagline[7]", + "entityName" : "Tagline", + "entityKind" : "ParagraphNode", + "parentPath" : "TealPulseCv[0]", + "childIndex" : 7, + "depth" : 2, + "layer" : 2, + "computedX" : 28.604, + "computedY" : 9.555, + "placementX" : 28.604, + "placementY" : 9.555, + "placementWidth" : 538.073, + "placementHeight" : 9.708, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.073, + "contentHeight" : 9.708, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + } ] +} diff --git a/qa/src/test/resources/visual-baselines/cv-v2-layered/teal_pulse-page-0.png b/qa/src/test/resources/visual-baselines/cv-v2-layered/teal_pulse-page-0.png new file mode 100644 index 000000000..6417b289f Binary files /dev/null and b/qa/src/test/resources/visual-baselines/cv-v2-layered/teal_pulse-page-0.png differ diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulse.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulse.java new file mode 100644 index 000000000..7301d71ed --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulse.java @@ -0,0 +1,220 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.cv.components.SectionLookup; +import com.demcha.compose.document.templates.cv.data.CvDocument; +import com.demcha.compose.document.templates.cv.data.CvSection; +import com.demcha.compose.document.templates.cv.data.EntriesSection; +import com.demcha.compose.document.templates.cv.data.ParagraphSection; +import com.demcha.compose.document.templates.cv.data.SkillsSection; + +import java.util.List; +import java.util.Objects; + +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.MARGIN_X; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.PAD_BOTTOM; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.PAD_TOP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.PAGE; + +/** + * Teal Pulse — a clinical one-page CV in five stacked bands: a heart crossed + * by a pulse beside the name, a contact strip divided by short rules, a + * two-column body carrying the competencies beside the summary and the roles, + * a three-column closing band, and a tracked line under a rule that ends in a + * small heart. + * + *

Exactly one of those bands is two-column, and both vertical rules on the + * sheet are the left border of the column to their right rather than lines + * placed beside it — so they are the grid: change a weight and the rules + * follow.

+ * + *

The page is the design's, not the caller's

+ * + *

The design was drawn on a raster whose proportion is not A4's, and every + * length on the sheet is a share of that grid, so the preset sets its own page + * size and margin. A session configured by the caller is overwritten. This is + * the opposite choice from {@link CharcoalGold}, which follows the page it is + * given, and it is made here because the sheet is a fixed composition rather + * than a layout that rescales.

+ * + *

One page, and what happens past it

+ * + *

The sheet is drawn for one page but does not insist on it, which sets it + * apart from the other ported presets in this package. The bands are stacked + * in the page flow, so a CV with more roles than the design holds carries the + * closing band onto a second page rather than losing anything — it will look + * like a one-page design that ran over, because that is what it is.

+ * + *

What cannot flow is the body itself: its two columns are a single row, + * and a row is atomic. A body taller than one page — the competencies beside + * the summary and every role — raises {@code AtomicNodeTooLargeException}, + * naming the node and the height it needed. The preset draws no cap of its + * own, because a CV that quietly loses a role is worse than one that refuses + * to compose. {@link TimelineMinimal} is the preset in this package that + * splits its own columns across pages.

+ * + *

How a document reaches its berth

+ * + *

The bands are fixed, so the preset assigns each section a berth by title + * rather than reading {@link + * com.demcha.compose.document.templates.cv.data.Slot}. It has six:

+ * + *
    + *
  • competencies, a {@link SkillsSection} — the narrow column;
  • + *
  • the summary, a {@link ParagraphSection}, and experience, an + * {@link EntriesSection} whose bodies are one highlight per line — the + * wide column;
  • + *
  • education, certifications and the closing facts, + * {@link EntriesSection}s — the three columns of the closing band;
  • + *
  • the tagline, a {@link ParagraphSection} whose body is the line under + * the closing rule. Only its body is drawn: the title is how the + * document names the berth, not something the sheet prints.
  • + *
+ * + *

A section whose title matches no berth is not drawn, and a berth with no + * section takes its badge and heading with it. The competencies are skills + * without levels: this design writes them as dotted lines, so a rating would + * have nowhere to go.

+ * + *

The marks, the tracking and the links

+ * + *

Every mark is chrome — the brand's two halves, the four contact glyphs, + * the five badges and the closing heart — so no document names one, and the + * badge each band carries is fixed by the berth it heads.

+ * + *

Headings are letter-spaced by writing each character as its own run with + * a small space between: a text style carries no letter-spacing, and the gap + * is split across several spacers because a run's size sets its line's height + * as well as its advance.

+ * + *

The email, the phone and each link are reachable from the PDF, with the + * {@code mailto:} and {@code tel:} targets built from the values. A link is + * drawn as its own label with the address behind it — {@code Link("LinkedIn", + * "https://…")} sets the word and links the URL — so the strip's gaps do not + * depend on how long a profile's address happens to be. A role and a degree + * become links when their entry carries {@code CvEntry.link()}.

+ * + *

Fonts

+ * + *

The sheet is set in Lato, with Poppins for the name and Barlow Condensed + * for the headings. The templates artifact carries none of them — register + * them on the session, or the engine substitutes and the measured geometry + * here no longer matches the type sitting in it.

+ * + *

Usage:

+ *
{@code
+ * DocumentTemplate template = TealPulse.create();
+ * template.compose(session, cv);
+ * }
+ * + * @since 2.2.3 + */ +public final class TealPulse { + + /** Stable identifier of this preset. */ + public static final String ID = "teal-pulse"; + + /** Human-readable name of this preset. */ + public static final String DISPLAY_NAME = "Teal Pulse"; + + private static final List COMPETENCY_KEYS = + List.of("competencies", "competency", "skills", "expertise"); + private static final List SUMMARY_KEYS = + List.of("summary", "profile", "about"); + private static final List TAGLINE_KEYS = + List.of("tagline", "motto", "closing"); + private static final List EXPERIENCE_KEYS = + List.of("experience", "employment", "work history", "career"); + private static final List EDUCATION_KEYS = + List.of("education", "qualifications"); + private static final List CERTIFICATION_KEYS = + List.of("certifications", "certification", "licences", "licenses"); + private static final List FACT_KEYS = + List.of("additional", "details", "other"); + + private TealPulse() { + } + + /** + * Creates the template. + * + * @return a template composing a {@link CvDocument} + */ + public static DocumentTemplate create() { + return new Template(); + } + + private record Template() implements DocumentTemplate { + + @Override + public String id() { + return ID; + } + + @Override + public String displayName() { + return DISPLAY_NAME; + } + + @Override + public void compose(DocumentSession document, CvDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + List sections = doc.sections(); + + SkillsSection competencies = berth(sections, SkillsSection.class, COMPETENCY_KEYS); + // The tagline is claimed first: its own keys are narrow, but the + // summary's are broad enough to take a closing line titled loosely. + ParagraphSection tagline = berth(sections, ParagraphSection.class, TAGLINE_KEYS); + ParagraphSection summary = + berthExcept(sections, ParagraphSection.class, SUMMARY_KEYS, tagline); + EntriesSection experience = berth(sections, EntriesSection.class, EXPERIENCE_KEYS); + EntriesSection education = berth(sections, EntriesSection.class, EDUCATION_KEYS); + EntriesSection certifications = + berth(sections, EntriesSection.class, CERTIFICATION_KEYS); + EntriesSection facts = berth(sections, EntriesSection.class, FACT_KEYS); + + // The sheet is a fixed composition on the design's own page, so the + // preset sets both rather than following what the caller configured. + document.pageSize(PAGE).margin(DocumentInsets.zero()); + document.pageFlow(page -> { + page.name("TealPulseCv"); + page.spacing(0); + page.padding(new DocumentInsets(PAD_TOP, MARGIN_X, PAD_BOTTOM, MARGIN_X)); + TealPulseMasthead.render(page, doc.identity()); + TealPulseBody.render(page, competencies, summary, experience); + TealPulseClosing.render(page, education, certifications, facts, tagline); + }); + } + + /** + * The first section of the wanted shape whose title names this berth, + * or {@code null} when the document fills no such berth. + */ + private static T berth(List sections, + Class type, + List keys) { + return berthExcept(sections, type, keys, null); + } + + /** The same, skipping a section another berth has already claimed. */ + private static T berthExcept(List sections, + Class type, + List keys, + CvSection taken) { + for (CvSection section : sections) { + if (!type.isInstance(section) || section == taken) { + continue; + } + for (String key : keys) { + if (SectionLookup.titleContains(section.title(), key)) { + return type.cast(section); + } + } + } + return null; + } + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseBody.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseBody.java new file mode 100644 index 000000000..a0562a1c1 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseBody.java @@ -0,0 +1,239 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.ParagraphBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.node.DocumentLinkOptions; +import com.demcha.compose.document.node.RowVerticalAlign; +import com.demcha.compose.document.node.TextAlign; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.templates.cv.data.CvEntry; +import com.demcha.compose.document.templates.cv.data.CvSkill; +import com.demcha.compose.document.templates.cv.data.EntriesSection; +import com.demcha.compose.document.templates.cv.data.ParagraphSection; +import com.demcha.compose.document.templates.cv.data.SkillGroup; +import com.demcha.compose.document.templates.cv.data.SkillsSection; + +import java.util.ArrayList; +import java.util.List; + +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ACCENT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ACCENT_DEEP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_FONT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_TEXT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_TO_CLOSING_RULE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BULLET_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BULLET_LEADING; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BULLET_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.COMPETENCY_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.COMPETENCY_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.DASH_TO_HEADING; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.DIVIDER_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ENTRY_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ENTRY_HEADLINE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ENTRY_TO_BULLETS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.HEADER_TO_BODY; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.HEADER_TO_ENTRY; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.HEADING_FONT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.HEADING_TO_ITEMS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ITEMS_TO_CLOSING_RULE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.MAIN_PAD_LEFT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.MAIN_WEIGHT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.RULE_PALE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.RULE_SOFT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SIDEBAR_CLOSING_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SIDEBAR_CLOSING_RULE_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SIDEBAR_DASH_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SIDEBAR_DASH_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SIDEBAR_HEADING_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SIDEBAR_HEADING_TRACKING_EM; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SIDEBAR_PAD_TOP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SIDEBAR_WEIGHT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SUMMARY_LEADING; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SUMMARY_RIGHT_INSET; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SUMMARY_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SUMMARY_TO_EXPERIENCE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.compact; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.style; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.dottedLine; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.sectionHeader; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.tracked; + +/** + * The body row: the competencies in the narrow column, the summary and the + * roles in the wide one. + */ +final class TealPulseBody { + + private TealPulseBody() { + } + + /** + * The one two-column band on the sheet. + * + *

The rule between the columns is the main column's own left accent, + * not a drawn line. That is what makes its height follow the taller of the + * two columns, which is what the design shows: it runs well past the end + * of the competency list.

+ */ + static void render(PageFlowBuilder page, SkillsSection competencies, + ParagraphSection summary, EntriesSection experience) { + page.addRow("BodyGrid", row -> { + row.weights(SIDEBAR_WEIGHT, MAIN_WEIGHT); + row.verticalAlign(RowVerticalAlign.TOP); + row.spacing(0); + row.addSection("Sidebar", sidebar -> renderCompetencies(sidebar, competencies)); + row.addSection("Main", main -> { + main.spacing(0); + main.accentLeft(RULE_SOFT, DIVIDER_WIDTH); + main.padding(new DocumentInsets(0, 0, 0, MAIN_PAD_LEFT)); + renderSummary(main, summary); + renderExperience(main, experience); + }); + row.margin(new DocumentInsets(0, 0, BODY_TO_CLOSING_RULE, 0)); + }); + } + + // -- the narrow column ------------------------------------------------- + + /** A dash, a tracked heading, the dotted lines, and a pale closing rule. */ + private static void renderCompetencies(SectionBuilder section, SkillsSection competencies) { + section.spacing(0); + section.padding(new DocumentInsets(SIDEBAR_PAD_TOP, 0, 0, 0)); + if (competencies == null) { + return; + } + section.addLine(line -> line + .name("CompetenciesDash") + .horizontal(SIDEBAR_DASH_WIDTH) + .thickness(SIDEBAR_DASH_THICKNESS) + .color(ACCENT) + .margin(new DocumentInsets(0, 0, DASH_TO_HEADING, 0))); + + DocumentTextStyle headingStyle = style( + HEADING_FONT, SIDEBAR_HEADING_SIZE, ACCENT_DEEP, DocumentTextDecoration.BOLD); + ParagraphBuilder heading = new ParagraphBuilder() + .name("CompetenciesHeading") + .textStyle(headingStyle) + .align(TextAlign.LEFT) + .lineSpacing(1.0); + tracked(heading, competencies.title(), headingStyle, SIDEBAR_HEADING_TRACKING_EM); + section.add(heading.margin(new DocumentInsets(0, 0, HEADING_TO_ITEMS, 0)).build()); + + section.addSection("CompetencyItems", items -> { + items.spacing(COMPETENCY_GAP); + for (SkillGroup group : competencies.groups()) { + for (CvSkill skill : group.entries()) { + items.add(dottedLine("Competency_" + compact(skill.name()), skill.name(), + COMPETENCY_SIZE, 1.0)); + } + } + }); + section.addLine(line -> line + .name("CompetenciesClosingRule") + .horizontal(SIDEBAR_CLOSING_RULE_WIDTH) + .thickness(SIDEBAR_CLOSING_RULE_THICKNESS) + .color(RULE_PALE) + .margin(new DocumentInsets(ITEMS_TO_CLOSING_RULE, 0, 0, 0))); + } + + // -- the wide column --------------------------------------------------- + + /** + * The summary under its badge. + * + *

The paragraph carries a right inset because the design breaks its + * first line with a finger's width of the column still free: reproducing + * the line breaks means reproducing the measure, not only the type + * size.

+ * + *

The summary is one flowing block here, so a body written as several + * lines is joined into one and left to wrap. This is the one berth that + * does that — everywhere else a line the document wrote is a line the + * sheet draws.

+ */ + private static void renderSummary(SectionBuilder main, ParagraphSection summary) { + if (summary == null || summary.body().isBlank()) { + return; + } + main.add(sectionHeader(summary.title(), TealPulseIcons.SUMMARY, HEADER_TO_BODY)); + main.addParagraph(paragraph -> paragraph + .name("SummaryText") + .text(summary.body().replace(String.valueOf((char) 10), " ")) + .textStyle(style(BODY_FONT, SUMMARY_SIZE, BODY_TEXT, + DocumentTextDecoration.DEFAULT)) + .align(TextAlign.LEFT) + .lineSpacing(SUMMARY_LEADING) + .margin(new DocumentInsets(0, SUMMARY_RIGHT_INSET, SUMMARY_TO_EXPERIENCE, 0))); + } + + /** The roles, each a two-voice headline over its dotted highlights. */ + private static void renderExperience(SectionBuilder main, EntriesSection experience) { + if (experience == null || experience.entries().isEmpty()) { + return; + } + main.add(sectionHeader(experience.title(), TealPulseIcons.EXPERIENCE, HEADER_TO_ENTRY)); + List entries = experience.entries(); + for (int i = 0; i < entries.size(); i++) { + CvEntry entry = entries.get(i); + boolean last = i == entries.size() - 1; + int index = i; + DocumentTextStyle roleStyle = style( + BODY_FONT, ENTRY_HEADLINE_SIZE, ACCENT_DEEP, DocumentTextDecoration.BOLD); + DocumentTextStyle metaStyle = style( + BODY_FONT, ENTRY_HEADLINE_SIZE, BODY_TEXT, DocumentTextDecoration.BOLD); + main.addParagraph(paragraph -> { + paragraph.name("ExperienceHeadline_" + index) + .textStyle(metaStyle) + .align(TextAlign.LEFT) + .lineSpacing(1.0); + if (entry.link().isBlank()) { + paragraph.inlineText(entry.title(), roleStyle); + } else { + paragraph.inlineText(entry.title(), roleStyle, + new DocumentLinkOptions(entry.link())); + } + paragraph.inlineText(headlineTail(entry), metaStyle) + .margin(new DocumentInsets(0, 0, ENTRY_TO_BULLETS, 0)); + }); + main.addSection("ExperienceHighlights_" + index, highlights -> { + highlights.spacing(BULLET_GAP); + for (String item : lines(entry.body())) { + highlights.add(dottedLine( + "Highlight_" + index + "_" + compact(item), item, + BULLET_SIZE, BULLET_LEADING)); + } + highlights.margin(new DocumentInsets(0, 0, last ? 0 : ENTRY_GAP, 0)); + }); + } + } + + /** + * What follows the role in its headline: the employer, then the dates, + * joined the way the design joins them. A dash and a pipe are typography + * rather than content, so they live here and not in the document. + */ + private static String headlineTail(CvEntry entry) { + StringBuilder tail = new StringBuilder(); + if (!entry.subtitle().isBlank()) { + tail.append(' ').append((char) 0x2014).append(' ').append(entry.subtitle()); + } + if (!entry.date().isBlank()) { + tail.append(tail.isEmpty() ? " " : " | ").append(entry.date()); + } + return tail.toString(); + } + + /** A body, one entry per line the document wrote. */ + static List lines(String body) { + List out = new ArrayList<>(); + for (String line : body.split(String.valueOf((char) 10))) { + if (!line.isBlank()) { + out.add(line.strip()); + } + } + return out; + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseClosing.java new file mode 100644 index 000000000..a466210c0 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseClosing.java @@ -0,0 +1,235 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.ParagraphBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.dsl.ShapeBuilder; +import com.demcha.compose.document.node.DocumentLinkOptions; +import com.demcha.compose.document.node.DocumentNode; +import com.demcha.compose.document.node.LayerAlign; +import com.demcha.compose.document.node.RowVerticalAlign; +import com.demcha.compose.document.node.TextAlign; +import com.demcha.compose.document.style.ClipPolicy; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.templates.cv.data.CvEntry; +import com.demcha.compose.document.templates.cv.data.EntriesSection; +import com.demcha.compose.document.templates.cv.data.ParagraphSection; + +import java.util.List; + +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ACCENT_DEEP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_BODY_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_COL1_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_COL2_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_COL3_TEXT_INSET; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_COL3_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_DEGREE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_FACT_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_LEADING; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_LINE_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_PAD_LEFT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_TO_TAGLINE_RULE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_WEIGHT_1; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_WEIGHT_2; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_WEIGHT_3; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_FONT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_TEXT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CERT_ITEM_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CLOSING_RULE_TO_BAND; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CONTENT_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.DIVIDER_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.FULL_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.HEADING_FONT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.RULE_SOFT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.RULE_STRONG; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.TAGLINE_RULE_HEIGHT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.TAGLINE_RULE_TO_TEXT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.TAGLINE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.TAGLINE_TRACKING_EM; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.compact; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.style; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.columnHeader; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.dottedLine; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.icon; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.tracked; + +/** + * What closes the sheet: the three-column band and the tagline under its + * rule. + */ +final class TealPulseClosing { + + private TealPulseClosing() { + } + + static void render(PageFlowBuilder page, EntriesSection education, + EntriesSection certifications, EntriesSection facts, + ParagraphSection tagline) { + page.addLine(line -> line + .name("ClosingBandRule") + .horizontal(CONTENT_WIDTH) + .thickness(FULL_RULE_THICKNESS) + .color(RULE_STRONG) + .margin(new DocumentInsets(0, 0, CLOSING_RULE_TO_BAND, 0))); + renderBand(page, education, certifications, facts); + renderTagline(page, tagline); + } + + // -- the band ---------------------------------------------------------- + + /** + * The three closing columns. + * + *

The two rules between them are left accents on the second and third + * columns, so they sit on the band's own weight boundaries and cannot + * drift off the grid.

+ */ + private static void renderBand(PageFlowBuilder page, EntriesSection education, + EntriesSection certifications, EntriesSection facts) { + page.addRow("ClosingBand", row -> { + row.weights(BAND_WEIGHT_1, BAND_WEIGHT_2, BAND_WEIGHT_3); + row.verticalAlign(RowVerticalAlign.TOP); + row.spacing(0); + row.addSection("EducationColumn", column -> { + column.spacing(0); + renderEducation(column, education); + }); + row.addSection("CertificationsColumn", column -> { + column.spacing(0); + column.accentLeft(RULE_SOFT, DIVIDER_WIDTH); + column.padding(new DocumentInsets(0, 0, 0, BAND_PAD_LEFT)); + renderCertifications(column, certifications); + }); + row.addSection("AdditionalColumn", column -> { + column.spacing(0); + column.accentLeft(RULE_SOFT, DIVIDER_WIDTH); + column.padding(new DocumentInsets(0, 0, 0, BAND_PAD_LEFT)); + renderFacts(column, facts); + }); + row.margin(new DocumentInsets(0, 0, BAND_TO_TAGLINE_RULE, 0)); + }); + } + + /** Each degree as three stacked lines: the award, the campus, the years. */ + private static void renderEducation(SectionBuilder column, EntriesSection education) { + if (education == null || education.entries().isEmpty()) { + return; + } + column.add(columnHeader(education.title(), TealPulseIcons.EDUCATION, BAND_COL1_WIDTH)); + List entries = education.entries(); + for (int index = 0; index < entries.size(); index++) { + CvEntry entry = entries.get(index); + boolean last = index == entries.size() - 1; + column.add(bandLine("EducationDegree_" + index, entry.title(), BAND_DEGREE_SIZE, + DocumentTextDecoration.BOLD, BAND_LINE_GAP, entry.link())); + column.add(bandLine("EducationInstitution_" + index, entry.subtitle(), + BAND_BODY_SIZE, DocumentTextDecoration.DEFAULT, BAND_LINE_GAP, "")); + column.add(bandLine("EducationDates_" + index, entry.date(), BAND_BODY_SIZE, + DocumentTextDecoration.DEFAULT, last ? 0 : BAND_LINE_GAP, "")); + } + } + + /** One line of closing-band body text, carrying a link when it has one. */ + private static DocumentNode bandLine(String name, String text, double size, + DocumentTextDecoration decoration, double gapBelow, + String link) { + DocumentTextStyle textStyle = style(BODY_FONT, size, BODY_TEXT, decoration); + ParagraphBuilder paragraph = new ParagraphBuilder() + .name(name) + .text(text) + .textStyle(textStyle) + .align(TextAlign.LEFT) + .lineSpacing(1.0) + .margin(new DocumentInsets(0, 0, gapBelow, 0)); + if (link != null && !link.isBlank()) { + paragraph.link(new DocumentLinkOptions(link)); + } + return paragraph.build(); + } + + private static void renderCertifications(SectionBuilder column, EntriesSection section) { + if (section == null || section.entries().isEmpty()) { + return; + } + column.add(columnHeader(section.title(), TealPulseIcons.CERTIFICATIONS, + BAND_COL2_WIDTH)); + column.addSection("CertificationItems", items -> { + items.spacing(CERT_ITEM_GAP); + for (CvEntry entry : section.entries()) { + items.add(dottedLine("Certification_" + compact(entry.title()), entry.title(), + BAND_BODY_SIZE, 1.0)); + } + }); + } + + /** + * The closing facts: a bold label and its value as two runs of one + * wrapping paragraph, which is why a fact that runs to a second line + * starts it at the column edge rather than under its value. + */ + private static void renderFacts(SectionBuilder column, EntriesSection section) { + if (section == null || section.entries().isEmpty()) { + return; + } + column.add(columnHeader(section.title(), TealPulseIcons.ADDITIONAL, BAND_COL3_WIDTH)); + DocumentTextStyle labelStyle = + style(BODY_FONT, BAND_BODY_SIZE, BODY_TEXT, DocumentTextDecoration.BOLD); + DocumentTextStyle valueStyle = + style(BODY_FONT, BAND_BODY_SIZE, BODY_TEXT, DocumentTextDecoration.DEFAULT); + column.addSection("AdditionalFacts", facts -> { + facts.spacing(BAND_FACT_GAP); + facts.padding(new DocumentInsets(0, BAND_COL3_TEXT_INSET, 0, 0)); + for (CvEntry entry : section.entries()) { + facts.addParagraph(paragraph -> paragraph + .name("Fact_" + compact(entry.title())) + .textStyle(valueStyle) + .align(TextAlign.LEFT) + .lineSpacing(BAND_LEADING) + .inlineText(entry.title(), labelStyle) + .inlineText(" " + entry.body().replace(String.valueOf((char) 10), " "), + valueStyle) + .margin(DocumentInsets.zero())); + } + }); + } + + // -- the tagline ------------------------------------------------------- + + /** + * The rule and the heart are one terminal — the rule anchored to its left, + * the heart to its right — so the mark follows the rule's end whatever the + * content width becomes. The tracked line sits under it. + */ + private static void renderTagline(PageFlowBuilder page, ParagraphSection tagline) { + if (tagline == null || tagline.body().isBlank()) { + return; + } + double heartSize = TealPulseIcons.size(TealPulseIcons.HEART); + double heartLift = -(heartSize - TAGLINE_RULE_HEIGHT) / 2.0; + DocumentNode rule = new ShapeBuilder() + .name("TaglineRuleLine") + .size(CONTENT_WIDTH, FULL_RULE_THICKNESS) + .fillColor(RULE_STRONG) + .build(); + page.addContainer(container -> container + .name("TaglineRule") + .rectangle(CONTENT_WIDTH, TAGLINE_RULE_HEIGHT) + .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE) + .centerLeft(rule) + .position(icon(TealPulseIcons.HEART), 0, heartLift, LayerAlign.CENTER_RIGHT) + .margin(new DocumentInsets(0, 0, TAGLINE_RULE_TO_TEXT, 0))); + + DocumentTextStyle textStyle = + style(HEADING_FONT, TAGLINE_SIZE, ACCENT_DEEP, DocumentTextDecoration.BOLD); + ParagraphBuilder line = new ParagraphBuilder() + .name("Tagline") + .textStyle(textStyle) + .align(TextAlign.CENTER) + .lineSpacing(1.0); + tracked(line, tagline.body().replace(String.valueOf((char) 10), " "), textStyle, + TAGLINE_TRACKING_EM); + page.add(line.margin(DocumentInsets.zero()).build()); + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseIcons.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseIcons.java new file mode 100644 index 000000000..388e205e5 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseIcons.java @@ -0,0 +1,105 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.svg.SvgIcon; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Classpath loader for the Teal Pulse CV marks: the two halves of the brand + * mark, the four contact glyphs, the five badge glyphs and the heart that + * closes the sheet. + * + *

They ship inside the templates artifact under + * {@code templates/cv/teal-pulse/icons/} as SVG, so they scale with the box + * they are drawn into rather than being resampled. Parsed icons are cached + * per token, so a sheet that repeats a mark reads each file once per JVM.

+ * + *

Every mark here is chrome — this sheet draws the same ones whatever the + * CV says — so the tokens are constants and no document names one. Each + * carries the width it is drawn at: an icon's box is not its glyph, and the + * sizes are measurements off the drawing rather than a shared constant.

+ */ +final class TealPulseIcons { + + static final String BRAND_HEART = "brand-heart"; + static final String BRAND_PULSE = "brand-pulse"; + static final String LOCATION = "location"; + static final String EMAIL = "email"; + static final String PHONE = "phone"; + static final String LINKEDIN = "linkedin"; + static final String SUMMARY = "summary"; + static final String EXPERIENCE = "experience"; + static final String EDUCATION = "education"; + static final String CERTIFICATIONS = "certifications"; + static final String ADDITIONAL = "additional"; + static final String HEART = "heart"; + + private static final String ICON_ROOT = "/templates/cv/teal-pulse/icons/"; + + /** + * The width each mark is drawn at — its whole box, not the glyph inside, + * which sits a few points within it. These are points rather than + * measurements off the drawing: the asset resolver chose them per glyph so + * that each one's visible ink matches what the design shows. + */ + private static final Map SIZES = Map.ofEntries( + Map.entry(BRAND_HEART, 77.1), + Map.entry(BRAND_PULSE, 92.8), + Map.entry(LOCATION, 15.6), + Map.entry(EMAIL, 16.2), + Map.entry(PHONE, 14.3), + Map.entry(LINKEDIN, 13.6), + Map.entry(SUMMARY, 20.0), + Map.entry(EXPERIENCE, 20.0), + Map.entry(EDUCATION, 17.5), + Map.entry(CERTIFICATIONS, 17.5), + Map.entry(ADDITIONAL, 17.5), + Map.entry(HEART, 14.9)); + + private static final Map CACHE = new ConcurrentHashMap<>(); + + private TealPulseIcons() { + } + + /** + * The width one mark is drawn at. + * + * @param token one of the packaged tokens + * @return the drawn width in points + */ + static double size(String token) { + Double size = SIZES.get(token); + if (size == null) { + throw new IllegalStateException("No size for teal pulse CV icon '" + token + "'"); + } + return size; + } + + /** + * Reads one mark. + * + * @param token one of the packaged tokens + * @return the parsed icon + */ + static SvgIcon icon(String token) { + return CACHE.computeIfAbsent(token, TealPulseIcons::read); + } + + private static SvgIcon read(String token) { + String resourcePath = ICON_ROOT + token + ".svg"; + try (InputStream input = TealPulseIcons.class.getResourceAsStream(resourcePath)) { + if (input == null) { + throw new IllegalStateException("Missing teal pulse CV icon: " + resourcePath); + } + return SvgIcon.parse(new String(input.readAllBytes(), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException( + "Failed to read teal pulse CV icon: " + resourcePath, e); + } + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseMasthead.java new file mode 100644 index 000000000..d83774d96 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseMasthead.java @@ -0,0 +1,264 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.ParagraphBuilder; +import com.demcha.compose.document.dsl.PathBuilder; +import com.demcha.compose.document.dsl.RowBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.dsl.ShapeContainerBuilder; +import com.demcha.compose.document.node.DocumentLinkOptions; +import com.demcha.compose.document.node.DocumentNode; +import com.demcha.compose.document.node.InlineImageAlignment; +import com.demcha.compose.document.node.LayerAlign; +import com.demcha.compose.document.node.RowArrangement; +import com.demcha.compose.document.node.RowVerticalAlign; +import com.demcha.compose.document.node.TextAlign; +import com.demcha.compose.document.style.ClipPolicy; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentLineCap; +import com.demcha.compose.document.style.DocumentLineJoin; +import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.style.DocumentStroke; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.svg.SvgPath; +import com.demcha.compose.document.templates.core.identity.Contact; +import com.demcha.compose.document.templates.core.identity.Link; +import com.demcha.compose.document.templates.cv.data.CvIdentity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ACCENT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ACCENT_DEEP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_FONT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_TEXT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CONTACT_ICON_DROP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CONTACT_ICON_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CONTACT_PAIR_LIFT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CONTACT_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CONTACT_TO_RULE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.CONTENT_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.DISPLAY_FONT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.DISPLAY_TEXT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.FULL_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.LOGO_HEIGHT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.LOGO_TO_IDENTITY; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.LOGO_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.MARK_STROKE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.MASTHEAD_TO_CONTACT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.NAME_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.NAME_TO_ROLE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.NAME_TRACKING_EM; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.PULSE_BOX_HEIGHT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.PULSE_BOX_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.PULSE_DROP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.PULSE_SHIFT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ROLE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ROLE_TRACKING_EM; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.RULE_STRONG; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.RULE_TO_BODY; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SEPARATOR; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SEPARATOR_HEIGHT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SEPARATOR_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.compact; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.gapRun; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.style; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.icon; +import static com.demcha.compose.document.templates.cv.presets.TealPulseWidgets.tracked; + +/** + * The top of the sheet: the mark beside the name, the contact strip, and the + * rule that closes them. + */ +final class TealPulseMasthead { + + private TealPulseMasthead() { + } + + /** One channel of the contact strip: a mark, a value and where it points. */ + private record Channel(String token, String value, String href) { + } + + static void render(PageFlowBuilder page, CvIdentity identity) { + page.addRow("Masthead", row -> { + row.columns(DocumentRowColumn.fixed(LOGO_WIDTH), DocumentRowColumn.weight(1)); + row.verticalAlign(RowVerticalAlign.CENTER); + row.spacing(LOGO_TO_IDENTITY); + row.add(brandMark()); + row.addSection("Identity", section -> renderIdentity(section, identity)); + row.margin(new DocumentInsets(0, 0, MASTHEAD_TO_CONTACT, 0)); + }); + renderContactStrip(page, identity); + page.addLine(line -> line + .name("MastheadRule") + .horizontal(CONTENT_WIDTH) + .thickness(FULL_RULE_THICKNESS) + .color(RULE_STRONG) + .margin(new DocumentInsets(0, 0, RULE_TO_BODY, 0))); + } + + // -- the mark ---------------------------------------------------------- + + /** + * A heart outline crossed by a flat pulse. + * + *

No single glyph has this geometry — every heartbeat icon puts the + * pulse in the left half — so the mark is two vectors centred in one box. + * Each child is wider or taller than that box, and a shape container + * clamps an over-sized child to the top left rather than centring it, so + * each is pulled back by half its own overflow. Those two corrections + * exist only to cancel the clamp.

+ */ + private static DocumentNode brandMark() { + double heartSize = TealPulseIcons.size(TealPulseIcons.BRAND_HEART); + double heartLift = -(heartSize - LOGO_HEIGHT) / 2.0; + return new ShapeContainerBuilder() + .name("BrandMark") + .rectangle(LOGO_WIDTH, LOGO_HEIGHT) + .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE) + .position(icon(TealPulseIcons.BRAND_HEART), 0, heartLift, LayerAlign.CENTER) + .position(pulse(), PULSE_SHIFT, PULSE_DROP, LayerAlign.CENTER) + .build(); + } + + /** + * The pulse, drawn from the packaged glyph's own geometry at a box this + * preset chooses, so the line spans the mark while the spike keeps the + * height the design draws. The stroke is the design's, not the glyph's. + */ + private static DocumentNode pulse() { + SvgPath geometry = TealPulseIcons.icon(TealPulseIcons.BRAND_PULSE) + .layers().get(0).geometry(); + return new PathBuilder() + .name("BrandPulse") + .size(PULSE_BOX_WIDTH, PULSE_BOX_HEIGHT) + .svg(geometry) + .stroke(DocumentStroke.of(ACCENT, MARK_STROKE)) + .lineCap(DocumentLineCap.ROUND) + .lineJoin(DocumentLineJoin.ROUND) + .build(); + } + + // -- the name ---------------------------------------------------------- + + /** The tracked display name over the tracked role, both in capitals. */ + private static void renderIdentity(SectionBuilder section, CvIdentity identity) { + section.spacing(0); + DocumentTextStyle nameStyle = + style(DISPLAY_FONT, NAME_SIZE, DISPLAY_TEXT, DocumentTextDecoration.DEFAULT); + ParagraphBuilder name = new ParagraphBuilder() + .name("DisplayName") + .textStyle(nameStyle) + .align(TextAlign.LEFT) + .lineSpacing(1.0); + tracked(name, identity.name().full().toUpperCase(Locale.ROOT), nameStyle, + NAME_TRACKING_EM); + section.add(name.margin(new DocumentInsets(0, 0, NAME_TO_ROLE, 0)).build()); + + DocumentTextStyle roleStyle = + style(DISPLAY_FONT, ROLE_SIZE, ACCENT_DEEP, DocumentTextDecoration.BOLD); + ParagraphBuilder role = new ParagraphBuilder() + .name("ProfessionalTitle") + .textStyle(roleStyle) + .align(TextAlign.LEFT) + .lineSpacing(1.0); + tracked(role, identity.jobTitle().toUpperCase(Locale.ROOT), roleStyle, ROLE_TRACKING_EM); + section.add(role.margin(DocumentInsets.zero()).build()); + } + + // -- the strip --------------------------------------------------------- + + /** + * The contact strip: the three channels and a row per link, each a mark + * and a value, divided by short rules. + * + *

Each pair is one paragraph, so the row's distribution can never split + * a mark from its value. Spreading the children pins the run to both + * margins and makes the gaps even, which a fixed gap would reach only by + * luck of the font's metrics.

+ * + *

A link shows its own label and carries the address behind it, so the + * strip's gaps do not depend on how long a profile's URL happens to be.

+ */ + private static void renderContactStrip(PageFlowBuilder page, CvIdentity identity) { + List channels = channels(identity); + page.addRow("ContactStrip", row -> { + row.verticalAlign(RowVerticalAlign.CENTER); + row.arrangement(RowArrangement.SPACE_BETWEEN); + for (int index = 0; index < channels.size(); index++) { + if (index > 0) { + separator(row, index); + } + row.add(channelPair(channels.get(index), index)); + } + row.margin(new DocumentInsets(0, 0, CONTACT_TO_RULE, 0)); + }); + } + + /** The strip's order: where, then how to write, then how to call, then links. */ + private static List channels(CvIdentity identity) { + Contact contact = identity.contact(); + List channels = new ArrayList<>(); + channels.add(new Channel(TealPulseIcons.LOCATION, contact.address(), null)); + channels.add(new Channel(TealPulseIcons.EMAIL, contact.email(), + "mailto:" + contact.email())); + channels.add(new Channel(TealPulseIcons.PHONE, contact.phone(), telUri(contact.phone()))); + for (Link link : identity.links()) { + channels.add(new Channel(TealPulseIcons.LINKEDIN, link.label(), link.url())); + } + return channels; + } + + private static DocumentNode channelPair(Channel channel, int index) { + DocumentTextStyle textStyle = + style(BODY_FONT, CONTACT_SIZE, BODY_TEXT, DocumentTextDecoration.DEFAULT); + DocumentLinkOptions link = channel.href() == null || channel.href().isBlank() + ? null + : new DocumentLinkOptions(channel.href()); + ParagraphBuilder pair = new ParagraphBuilder() + .name("Contact_" + index + "_" + compact(channel.token())) + .textStyle(textStyle) + .align(TextAlign.LEFT) + .lineSpacing(1.0) + .inlineSvgIcon(TealPulseIcons.icon(channel.token()), + TealPulseIcons.size(channel.token()), InlineImageAlignment.CENTER, + CONTACT_ICON_DROP, link); + String value = gapRun(CONTACT_ICON_GAP, CONTACT_SIZE) + channel.value(); + if (link == null) { + pair.inlineText(value, textStyle); + } else { + pair.inlineText(value, textStyle, link); + } + // The pair still sits below the axis the design uses. It is centred in + // a row whose height the separators set, so a NEGATIVE top margin of + // twice the wanted lift raises it by half of it. Negative rather than a + // positive bottom margin: both shift a centred child equally, but a + // bottom margin would grow the box past the separators and take the + // whole strip — and everything under it — down the page. + return pair.margin(new DocumentInsets(-2 * CONTACT_PAIR_LIFT, 0, 0, 0)).build(); + } + + private static void separator(RowBuilder row, int index) { + row.addLine(line -> line + .name("ContactSeparator_" + index) + .vertical(SEPARATOR_HEIGHT) + .thickness(SEPARATOR_THICKNESS) + .color(SEPARATOR) + .margin(DocumentInsets.zero())); + } + + /** + * The dial target for a phone number: its digits, keeping a leading + * {@code +} so an international number stays international, and dropping a + * parenthesised trunk prefix the way a caller dialling from abroad does. + */ + private static String telUri(String phone) { + String dialled = phone.replaceAll("\\(0+\\)", ""); + String digits = dialled.replaceAll("[^0-9]", ""); + return digits.isEmpty() + ? null + : "tel:" + (phone.trim().startsWith("+") ? "+" : "") + digits; + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseStyles.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseStyles.java new file mode 100644 index 000000000..5f8cb68e4 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseStyles.java @@ -0,0 +1,313 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.font.FontName; + +/** + * The measured geometry, palette and type scale of the Teal Pulse CV. + * + *

Every length is a measurement off one grid

+ * + *

The design was drawn on a 1103×1426 raster, so each length here is + * written as {@link #px} of a pixel measured off it and {@code px} is the one + * conversion into points. Re-measuring the drawing therefore changes one + * number per length and never a chain of them — and the page itself takes + * that grid's proportion rather than A4's, which is why this preset sets its + * own page.

+ * + *

Leading is not a multiple of the type size

+ * + *

A paragraph's line pitch on this engine is about + * {@code 1.216 × size + 0.872 × lineSpacing}, so the leading constants below + * are the values that produce the drawn pitch rather than ratios of it. A + * single-line paragraph ignores leading entirely and takes {@code 1.216 × + * size}, which is why the bullet rhythm is section spacing instead.

+ */ +final class TealPulseStyles { + + private TealPulseStyles() { + } + + // -- the grid ---------------------------------------------------------- + + /** The raster the design was drawn on. */ + static final double REFERENCE_WIDTH_PX = 1103.0; + static final double REFERENCE_HEIGHT_PX = 1426.0; + + static final double PAGE_WIDTH = 595.28; + static final double PAGE_HEIGHT = PAGE_WIDTH * REFERENCE_HEIGHT_PX / REFERENCE_WIDTH_PX; + static final DocumentPageSize PAGE = DocumentPageSize.of(PAGE_WIDTH, PAGE_HEIGHT); + + /** + * One measurement off the drawing, in points. + * + * @param referencePixels the pixel measured on the 1103-wide raster + * @return the same distance on the page + */ + static double px(double referencePixels) { + return referencePixels * PAGE_WIDTH / REFERENCE_WIDTH_PX; + } + + static final double MARGIN_X = px(53); + static final double PAD_TOP = px(34); + static final double PAD_BOTTOM = px(14); + static final double CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN_X; + + /** The sidebar runs from the left margin to the column rule. */ + static final double SIDEBAR_WEIGHT = 0.26533; + static final double MAIN_WEIGHT = 1.0 - SIDEBAR_WEIGHT; + static final double MAIN_PAD_LEFT = px(32); + static final double MAIN_CONTENT_WIDTH = CONTENT_WIDTH * MAIN_WEIGHT - MAIN_PAD_LEFT; + + /** + * The closing band's first divider is collinear with the body's column + * rule, so its first weight is the sidebar's — that is the shared grid, + * not a coincidence to be measured twice. + */ + static final double BAND_WEIGHT_1 = SIDEBAR_WEIGHT; + static final double BAND_WEIGHT_2 = 0.37789; + static final double BAND_WEIGHT_3 = 1.0 - BAND_WEIGHT_1 - BAND_WEIGHT_2; + + static final double BAND_PAD_LEFT = px(36); + + /** + * Two right insets, because the design uses two: a heading's underline + * stops well short of the divider while the body under it runs on almost + * to the rule. One inset for both would either wrap the longest line or + * overrun every underline. + */ + static final double BAND_RULE_INSET = px(40); + static final double BAND_COL1_WIDTH = CONTENT_WIDTH * BAND_WEIGHT_1 - BAND_RULE_INSET; + static final double BAND_COL2_WIDTH = + CONTENT_WIDTH * BAND_WEIGHT_2 - BAND_PAD_LEFT - BAND_RULE_INSET; + static final double BAND_COL3_WIDTH = CONTENT_WIDTH * BAND_WEIGHT_3 - BAND_PAD_LEFT; + + /** + * Two blocks wrap inside a measure narrower than the column holding them, + * and the design is unambiguous about both: the summary breaks with a + * finger's width of its column still free, and so does the first fact. + * Reproducing the line breaks means reproducing the measure, not only the + * type size. + */ + static final double SUMMARY_RIGHT_INSET = px(105); + static final double BAND_COL3_TEXT_INSET = px(79); + + // -- type -------------------------------------------------------------- + + static final FontName DISPLAY_FONT = FontName.POPPINS; + + /** + * The drawing's heading face sets about a fifth narrower per cap height + * than the body's bold, so the headings take a condensed face and keep + * both dimensions rather than shrinking to fit and losing the cap height. + */ + static final FontName HEADING_FONT = FontName.BARLOW_CONDENSED; + static final FontName BODY_FONT = FontName.LATO; + + static final double NAME_SIZE = 39.5; + static final double ROLE_SIZE = 13.84; + static final double CONTACT_SIZE = 7.86; + static final double SIDEBAR_HEADING_SIZE = 10.4; + static final double COMPETENCY_SIZE = 9.09; + static final double MAIN_HEADING_SIZE = 12.7; + static final double SUMMARY_SIZE = 9.91; + static final double ENTRY_HEADLINE_SIZE = 8.90; + static final double BULLET_SIZE = 8.16; + static final double BAND_HEADING_SIZE = 9.6; + static final double BAND_BODY_SIZE = 8.6; + static final double BAND_DEGREE_SIZE = 10.55; + static final double TAGLINE_SIZE = 8.09; + + static final double SUMMARY_LEADING = 5.37; + static final double BULLET_LEADING = 1.0; + static final double BAND_LEADING = 4.71; + + // -- tracking ---------------------------------------------------------- + + /** + * A space's advance as a fraction of the type size. Tracking is written as + * spacer runs between the letters because a text style carries no + * letter-spacing, and this is what turns a wanted em fraction into a + * number of spaces. + */ + static final double SPACE_ADVANCE_EM = 0.25; + + static final double NAME_TRACKING_EM = 0.045; + static final double ROLE_TRACKING_EM = 0.43; + static final double MAIN_HEADING_TRACKING_EM = 0.124; + static final double SIDEBAR_HEADING_TRACKING_EM = 0.251; + static final double BAND_HEADING_TRACKING_EM = 0.215; + static final double TAGLINE_TRACKING_EM = 0.239; + + // -- palette ----------------------------------------------------------- + + static final DocumentColor ACCENT = DocumentColor.rgb(6, 100, 112); + static final DocumentColor ACCENT_DEEP = DocumentColor.rgb(0, 80, 95); + static final DocumentColor RULE_STRONG = DocumentColor.rgb(0, 88, 104); + static final DocumentColor RULE_SOFT = DocumentColor.rgb(119, 174, 182); + static final DocumentColor RULE_PALE = DocumentColor.rgb(177, 204, 207); + static final DocumentColor SEPARATOR = DocumentColor.rgb(54, 125, 135); + static final DocumentColor DISPLAY_TEXT = DocumentColor.rgb(34, 36, 50); + static final DocumentColor BODY_TEXT = DocumentColor.rgb(17, 17, 17); + static final DocumentColor PAPER = DocumentColor.WHITE; + + // -- the mark ---------------------------------------------------------- + + static final double LOGO_WIDTH = px(132); + static final double LOGO_HEIGHT = px(105); + static final double LOGO_TO_IDENTITY = px(39); + static final double MARK_STROKE = px(3); + + /** The pulse crosses the heart below its centre, not through it. */ + static final double PULSE_DROP = px(6.5); + + /** + * The pulse's box is its INK box: a path is mapped onto the size given by + * its own bounds rather than its viewBox. Its height is the spike's, and + * that is why the pulse is a path and not an icon — the drawing's line is + * flatter than any heartbeat glyph, and an icon keeps its aspect. + */ + static final double PULSE_BOX_WIDTH = px(172); + static final double PULSE_BOX_HEIGHT = px(82); + + /** + * A shape container clamps an over-wide child to the left exactly as it + * top-clamps an over-tall one, so the pulse is pulled back by half its own + * overflow. + */ + static final double PULSE_SHIFT = -(PULSE_BOX_WIDTH - LOGO_WIDTH) / 2.0; + + // -- rules, badges and dots -------------------------------------------- + + static final double DIVIDER_WIDTH = px(2); + static final double FULL_RULE_THICKNESS = px(2.5); + static final double HEADER_RULE_THICKNESS = px(2.5); + static final double BAND_RULE_THICKNESS = px(3); + static final double SIDEBAR_DASH_WIDTH = px(54); + static final double SIDEBAR_DASH_THICKNESS = px(3); + static final double SIDEBAR_CLOSING_RULE_WIDTH = px(216); + static final double SIDEBAR_CLOSING_RULE_THICKNESS = px(3); + static final double TAGLINE_RULE_HEIGHT = px(21); + + static final double BADGE_MAIN = px(58); + static final double BADGE_BAND = px(51); + static final double SECTION_GAP = px(17); + static final double BAND_HEADER_GAP = px(18); + static final double DOT_DIAMETER = px(6); + static final double DOT_GAP = px(17); + + static final double SEPARATOR_HEIGHT = px(38); + static final double SEPARATOR_THICKNESS = px(1.5); + + /** + * A contact icon is twice the height of the value beside it, so it is the + * icon that sets the line box: centring puts the icon on that box's middle + * while the value still sits near its foot, and the pair reads as standing + * at two levels. These two corrections put both back where the drawing has + * them — the first raises the pair, the second returns the icon to the + * value's own axis. + */ + static final double CONTACT_ICON_DROP = -px(4); + static final double CONTACT_PAIR_LIFT = px(4); + + /** + * The gap asked of the type, not the gap that appears: an icon's box + * carries its own bearing, so about half the drawn gap is requested and + * the bearing makes up the rest. + */ + static final double CONTACT_ICON_GAP = px(7.5); + + // -- the rhythm between blocks ----------------------------------------- + + static final double MASTHEAD_TO_CONTACT = px(17); + static final double CONTACT_TO_RULE = px(23); + static final double RULE_TO_BODY = px(29); + static final double NAME_TO_ROLE = px(0); + static final double SIDEBAR_PAD_TOP = px(9); + static final double DASH_TO_HEADING = px(14); + static final double HEADING_TO_ITEMS = px(35); + static final double COMPETENCY_GAP = px(29.2); + static final double ITEMS_TO_CLOSING_RULE = px(50); + static final double HEADER_TO_BODY = px(20); + static final double SUMMARY_TO_EXPERIENCE = px(29); + static final double HEADER_TO_ENTRY = px(14); + static final double ENTRY_TO_BULLETS = px(13); + static final double ENTRY_GAP = px(34); + static final double BODY_TO_CLOSING_RULE = px(30); + static final double CLOSING_RULE_TO_BAND = px(27); + static final double BAND_HEADING_TO_UNDERLINE = px(9.5); + static final double BAND_HEADER_TO_BODY = px(22); + + /** + * The closing-band heading does not sit on its badge's centre line: the + * stack is seated below it so the underline clears the badge's lower half + * rather than cutting through it. + */ + static final double BAND_HEADING_DROP = px(12); + static final double BAND_LINE_GAP = px(10.5); + static final double BAND_FACT_GAP = px(14.6); + static final double CERT_ITEM_GAP = px(12.9); + static final double BAND_TO_TAGLINE_RULE = px(22); + static final double TAGLINE_RULE_TO_TEXT = px(1.6); + + /** + * Bullet pitch is section spacing, not line spacing: a single-line + * paragraph takes the font's own line height whatever leading it is given, + * so the drawn pitch has to be the gap between paragraphs. + */ + static final double BULLET_GAP = px(7.9); + + // -- helpers ----------------------------------------------------------- + + static DocumentTextStyle style(FontName font, double size, DocumentColor color, + DocumentTextDecoration decoration) { + return DocumentTextStyle.builder() + .fontName(font) + .size(size) + .color(color) + .decoration(decoration) + .build(); + } + + /** + * The size a spacer run is set at so that {@code runs} of them advance the + * wanted fraction of an em. + */ + static DocumentTextStyle spacerStyle(DocumentTextStyle textStyle, double trackingEm, + int runs) { + return textStyle.withSize(trackingEm * textStyle.size() / (runs * SPACE_ADVANCE_EM)); + } + + /** + * How many spacer characters carry one gap. + * + *

A run's size sets the line's height as well as its advance, so a + * single space wide enough for heavy tracking would make the line taller + * than its own type. Splitting the gap across several smaller spaces keeps + * every spacer below the text size.

+ */ + static int spacerRuns(double trackingEm) { + return Math.max(1, (int) Math.ceil(trackingEm / SPACE_ADVANCE_EM)); + } + + /** + * Spaces that advance about {@code width} points at {@code size}. + * + *

Deliberately not one resized space: a run's size sets the line's + * height as well as its advance, so a single space wide enough for the gap + * would make its line several times too tall. The count follows the size, + * so changing the type scale moves the gap with it.

+ */ + static String gapRun(double width, double size) { + int spaces = (int) Math.max(1, Math.round(width / (SPACE_ADVANCE_EM * size))); + return " ".repeat(spaces); + } + + /** A node name built from a heading, with the spacing taken out of it. */ + static String compact(String value) { + return value == null ? "" : value.replaceAll("[^A-Za-z0-9]", ""); + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseWidgets.java new file mode 100644 index 000000000..1a40b3a73 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseWidgets.java @@ -0,0 +1,234 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.dsl.ParagraphBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.dsl.ShapeBuilder; +import com.demcha.compose.document.dsl.ShapeContainerBuilder; +import com.demcha.compose.document.node.DocumentNode; +import com.demcha.compose.document.node.LayerAlign; +import com.demcha.compose.document.node.TextAlign; +import com.demcha.compose.document.style.ClipPolicy; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; + +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ACCENT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.ACCENT_DEEP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BADGE_BAND; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BADGE_MAIN; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_HEADER_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_HEADER_TO_BODY; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_HEADING_DROP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_HEADING_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_HEADING_TO_UNDERLINE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_HEADING_TRACKING_EM; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BAND_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_FONT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.BODY_TEXT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.DOT_DIAMETER; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.DOT_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.HEADER_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.HEADING_FONT; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.MAIN_CONTENT_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.MAIN_HEADING_SIZE; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.MAIN_HEADING_TRACKING_EM; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.PAPER; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SECTION_GAP; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.compact; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.gapRun; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.spacerRuns; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.spacerStyle; +import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.style; + +/** + * The idioms this sheet repeats: the tracked run, the badge, the dotted line + * and the two heading bars. + */ +final class TealPulseWidgets { + + private TealPulseWidgets() { + } + + // -- tracking ---------------------------------------------------------- + + /** + * A letter-spaced run. + * + *

A text style carries no letter-spacing, so every character is its own + * run and the gaps between them are spaces set small enough to advance the + * wanted fraction of an em. The gap is split across several spacers rather + * than carried on one, because a run's size sets its line's height as well + * as its advance.

+ * + * @param paragraph the paragraph to write into + * @param text the text to space out + * @param textStyle the style of the letters + * @param trackingEm the gap as a fraction of an em + */ + static void tracked(ParagraphBuilder paragraph, String text, DocumentTextStyle textStyle, + double trackingEm) { + int runs = spacerRuns(trackingEm); + DocumentTextStyle spacer = spacerStyle(textStyle, trackingEm, runs); + for (int index = 0; index < text.length(); index++) { + paragraph.inlineText(String.valueOf(text.charAt(index)), textStyle); + if (index + 1 < text.length()) { + paragraph.inlineText(" ".repeat(runs), spacer); + } + } + } + + /** + * The same, with the paper laid behind every run, so a rule the heading is + * drawn over shows only past the end of the words. + */ + static void knockedOut(ParagraphBuilder paragraph, String text, + DocumentTextStyle textStyle, double trackingEm) { + int runs = spacerRuns(trackingEm); + DocumentTextStyle spacer = spacerStyle(textStyle, trackingEm, runs); + for (int index = 0; index < text.length(); index++) { + knockedOutRun(paragraph, String.valueOf(text.charAt(index)), textStyle); + if (index + 1 < text.length()) { + knockedOutRun(paragraph, " ".repeat(runs), spacer); + } + } + } + + /** One knocked-out run. No horizontal padding, so neighbours abut. */ + private static void knockedOutRun(ParagraphBuilder paragraph, String text, + DocumentTextStyle textStyle) { + paragraph.inlineHighlight(text, textStyle, PAPER, 0, + new DocumentInsets(HEADER_RULE_THICKNESS, 0, HEADER_RULE_THICKNESS, 0)); + } + + // -- marks ------------------------------------------------------------- + + /** A teal disc owning a white glyph — the pair moves and scales as one. */ + static DocumentNode badge(String name, String token, double diameter) { + return new ShapeContainerBuilder() + .name("Badge_" + compact(name)) + .circle(diameter) + .fillColor(ACCENT) + .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE) + .center(icon(token)) + .build(); + } + + /** + * One mark as a node. It is wrapped in a section rather than placed + * directly, so the vector path reaches the page rather than a coloured + * stand-in shaped like its box. + */ + static DocumentNode icon(String token) { + return new SectionBuilder() + .name("Icon_" + compact(token)) + .spacing(0) + .padding(DocumentInsets.zero()) + .margin(DocumentInsets.zero()) + .addSvgIcon(TealPulseIcons.icon(token), TealPulseIcons.size(token)) + .build(); + } + + // -- lines ------------------------------------------------------------- + + /** + * A teal dot leading a near-black label. + * + *

Not a list: a list marker takes its item's own text colour, and here + * the dot and the label are deliberately different colours.

+ */ + static DocumentNode dottedLine(String name, String text, double size, double leading) { + DocumentTextStyle textStyle = + style(BODY_FONT, size, BODY_TEXT, DocumentTextDecoration.DEFAULT); + return new ParagraphBuilder() + .name(name) + .textStyle(textStyle) + .align(TextAlign.LEFT) + .lineSpacing(leading) + .dot(DOT_DIAMETER, ACCENT) + .inlineText(gapRun(DOT_GAP, size) + text, textStyle) + .margin(DocumentInsets.zero()) + .build(); + } + + // -- heading bars ------------------------------------------------------ + + /** + * The main column's heading: a badge, the words, and a rule running from + * them to the right margin. + * + *

The obvious construction is unavailable here. A row of fixed / auto / + * weighted cells would let the layout derive the rule's length from the + * heading's width, but a row cannot nest inside a row cell and the main + * column is a row cell; the alternative, a shape container placing + * children by anchor, needs every width up front.

+ * + *

So the rule is drawn at full length from just past the badge and the + * heading is laid over it with the paper knocked out behind every run. The + * rule then shows exactly where the heading is not, which is the picture an + * auto column would have produced, and its visible length still follows the + * words rather than a width guessed while authoring.

+ */ + static DocumentNode sectionHeader(String title, String token, double gapBelow) { + double railStart = BADGE_MAIN + SECTION_GAP; + DocumentNode rail = new ShapeBuilder() + .name("SectionHeaderRule_" + compact(title)) + .size(MAIN_CONTENT_WIDTH - railStart, HEADER_RULE_THICKNESS) + .fillColor(ACCENT) + .build(); + DocumentTextStyle headingStyle = style( + HEADING_FONT, MAIN_HEADING_SIZE, ACCENT_DEEP, DocumentTextDecoration.BOLD); + ParagraphBuilder heading = new ParagraphBuilder() + .name("SectionHeading_" + compact(title)) + .textStyle(headingStyle) + .align(TextAlign.LEFT) + .lineSpacing(1.0); + knockedOut(heading, title, headingStyle, MAIN_HEADING_TRACKING_EM); + knockedOutRun(heading, gapRun(SECTION_GAP, MAIN_HEADING_SIZE), headingStyle); + return new ShapeContainerBuilder() + .name("SectionHeader_" + compact(title)) + .rectangle(MAIN_CONTENT_WIDTH, BADGE_MAIN) + .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE) + .centerLeft(badge(title, token, BADGE_MAIN)) + .position(rail, railStart, 0, LayerAlign.CENTER_LEFT, 0) + .position(heading.margin(DocumentInsets.zero()).build(), + railStart, 0, LayerAlign.CENTER_LEFT, 1) + .margin(new DocumentInsets(0, 0, gapBelow, 0)) + .build(); + } + + /** + * The closing band's heading: the rule sits UNDER the words rather than + * after them, so its width is derivable — the column less the badge and + * the gap — and nothing has to be knocked out. + */ + static DocumentNode columnHeader(String title, String token, double columnWidth) { + DocumentTextStyle headingStyle = style( + HEADING_FONT, BAND_HEADING_SIZE, ACCENT_DEEP, DocumentTextDecoration.BOLD); + ParagraphBuilder heading = new ParagraphBuilder() + .name("ColumnHeading_" + compact(title)) + .textStyle(headingStyle) + .align(TextAlign.LEFT) + .lineSpacing(1.0); + tracked(heading, title, headingStyle, BAND_HEADING_TRACKING_EM); + double stackStart = BADGE_BAND + BAND_HEADER_GAP; + DocumentNode stack = new SectionBuilder() + .name("ColumnHeadingStack_" + compact(title)) + .spacing(0) + .add(heading.margin(DocumentInsets.zero()).build()) + .addLine(line -> line + .name("ColumnHeadingRule_" + compact(title)) + .horizontal(columnWidth - stackStart) + .thickness(BAND_RULE_THICKNESS) + .color(ACCENT) + .margin(new DocumentInsets(BAND_HEADING_TO_UNDERLINE, 0, 0, 0))) + .build(); + return new ShapeContainerBuilder() + .name("ColumnHeader_" + compact(title)) + .rectangle(columnWidth, BADGE_BAND) + .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE) + .centerLeft(badge(title, token, BADGE_BAND)) + .position(stack, stackStart, BAND_HEADING_DROP, LayerAlign.CENTER_LEFT) + .margin(new DocumentInsets(0, 0, BAND_HEADER_TO_BODY, 0)) + .build(); + } +} diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/additional.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/additional.svg new file mode 100644 index 000000000..50719fb48 --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/additional.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/brand-heart.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/brand-heart.svg new file mode 100644 index 000000000..e678eff0f --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/brand-heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/brand-pulse.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/brand-pulse.svg new file mode 100644 index 000000000..b02fd7af5 --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/brand-pulse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/certifications.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/certifications.svg new file mode 100644 index 000000000..972d65546 --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/certifications.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/education.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/education.svg new file mode 100644 index 000000000..66eb8e377 --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/email.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/email.svg new file mode 100644 index 000000000..291f1c368 --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/experience.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/experience.svg new file mode 100644 index 000000000..d474a2838 --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/experience.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/heart.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/heart.svg new file mode 100644 index 000000000..20d5f5076 --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/linkedin.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/linkedin.svg new file mode 100644 index 000000000..3ed716e9e --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/linkedin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/location.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/location.svg new file mode 100644 index 000000000..876c7101e --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/location.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/phone.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/phone.svg new file mode 100644 index 000000000..bfab7b00d --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/teal-pulse/icons/summary.svg b/templates/src/main/resources/templates/cv/teal-pulse/icons/summary.svg new file mode 100644 index 000000000..173514856 --- /dev/null +++ b/templates/src/main/resources/templates/cv/teal-pulse/icons/summary.svg @@ -0,0 +1 @@ + \ No newline at end of file