diff --git a/CHANGELOG.md b/CHANGELOG.md index 32a558537..26aee1bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,34 @@ follow semantic versioning; release dates are ISO 8601. ### Templates +- **A banded single-column CV preset: `VioletGrid`.** A one-page sheet with no page-level + grid at all: a two-tone name beside the contact list, three opening lines, a six-up + grid of marked skills divided by dotted rules, a strip of tools on inline discs, the + roles held on a dated timeline whose dates sit outside the rail, the projects behind + tinted tiles, education and languages side by side, and a tinted band closing on a + quotation. Ships as `cv.presets.VioletGrid` on the existing `CvDocument` model, + porting the rendered layout of a published standalone template; like `SlateOrange` it + reads a rated language's wording from `CvSkill.note`. Every split is local to one + band — the masthead splits where the contact marks begin, the timeline into dates, + rail and content, a project into tile and copy, the credentials into two halves — and + none of the four knows about the others, which is why every horizontal arrangement is + either a top-level row or a table and none of them nests. Three marks are + relationships rather than lengths: a section rule starts where its heading ends + because it is a weighted column taking what an auto column leaves; the timeline rail + is the left border of every entry but the last, so consecutive entries butt into one + line; and a project's hairline is its copy's left border, so it is exactly as tall as + the copy. Seven berths reach their sections by title, including a quotation berth + whose body is drawn and whose title is not. Unlike its ported siblings this sheet + flows: each experience entry is held together, so a longer CV runs onto a second page + rather than cutting a role in half. As on the presets before it a link is drawn as its + own label with the address behind it, the one deliberate departure from the ported + sheet, measured at 1 882 of 2 173 720 pixels. Guarded by a smoke test (including the + unknown-mark data error, a skill with no mark, the two-tone name, the quotation's + title staying off the sheet, the link targets on every kind of title, an identity with + no links, a document with nothing but an identity, and the run onto a second page), an + exact layout snapshot and a pixel-parity gate; the examples showcase gains + `cv-violet-grid-v2`. + - **A masthead-and-rail CV preset: `SlateOrange`.** A one-page sheet built as a full-bleed slate band over a two-column body: an orange monogram tile beside the name, the role line and a tracked specialism strip, with the contact lines across an orange diff --git a/assets/readme/examples/cv-violet-grid-v2.pdf b/assets/readme/examples/cv-violet-grid-v2.pdf new file mode 100644 index 000000000..eb94465c6 Binary files /dev/null and b/assets/readme/examples/cv-violet-grid-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 4a505b362..d2d1ae590 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -81,6 +81,7 @@ import com.demcha.examples.templates.coverletter.CvTimelineMinimalLetterV2Example; import com.demcha.examples.templates.cv.v2.CharcoalGoldExample; import com.demcha.examples.templates.cv.v2.SlateOrangeExample; +import com.demcha.examples.templates.cv.v2.VioletGridExample; import com.demcha.examples.templates.cv.v2.TealPulseExample; import com.demcha.examples.templates.cv.v2.TerracottaRailExample; import com.demcha.examples.templates.cv.v2.CvBlueBannerExample; @@ -160,6 +161,7 @@ public static void main(String[] args) throws Exception { System.out.println("Generated: " + TerracottaRailExample.generate()); System.out.println("Generated: " + TealPulseExample.generate()); System.out.println("Generated: " + SlateOrangeExample.generate()); + System.out.println("Generated: " + VioletGridExample.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 8dba6c7fe..73059d99d 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java @@ -67,6 +67,7 @@ record Entry(String title, String description, List tags, String codeUrl 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"); cv("cv-slate-orange-v2", "SlateOrangeExample", "Slate Orange", "Full-bleed slate masthead with an orange monogram tile, over a sidebar of marked competencies, trophied achievements and rated languages beside a profile, a dated experience rail and a credentials footer.", "sidebar", "masthead", "orange"); + cv("cv-violet-grid-v2", "VioletGridExample", "Violet Grid", "Single-column sheet in bands: a two-tone name beside the contact list, a six-up grid of marked skills on dotted rules, a tools strip, a dated timeline, tinted project tiles and a closing quotation.", "single-column", "grid", "violet"); // ===== 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/VioletGridSampleData.java b/examples/src/main/java/com/demcha/examples/support/VioletGridSampleData.java new file mode 100644 index 000000000..3266599a1 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/support/VioletGridSampleData.java @@ -0,0 +1,224 @@ +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; + +/** + * Shared sample data for the Violet Grid CV example. + * + *

The sample is the preset's reference content: a one-page CV with five + * contact lines including two links, three opening lines, six marked skills, + * eleven tools, three roles, two projects, a degree, three rated languages + * and the closing quotation.

+ * + *

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

+ */ +public final class VioletGridSampleData { + + /** The break a body stacks its lines on. */ + private static final String NEWLINE = String.valueOf((char) 10); + + private VioletGridSampleData() { + } + + /** The single-page reference CV. */ + public static CvDocument sample() { + List placements = new ArrayList<>(); + placements.add(new CvDocument.Placement(Slot.MAIN, summary())); + placements.add(new CvDocument.Placement(Slot.MAIN, skills())); + placements.add(new CvDocument.Placement(Slot.MAIN, tools())); + placements.add(new CvDocument.Placement(Slot.MAIN, experience())); + placements.add(new CvDocument.Placement(Slot.MAIN, projects())); + placements.add(new CvDocument.Placement(Slot.FOOTER, education())); + placements.add(new CvDocument.Placement(Slot.FOOTER, languages())); + placements.add(new CvDocument.Placement(Slot.FOOTER, quote())); + return new CvDocument(identity(), placements); + } + + private static CvIdentity identity() { + return new CvIdentity( + CvName.of("SOFIA", "MARTINEZ"), + "UX / UI DESIGNER", + new Contact("+1 (415) 555-7842", + "sofia.martinez.design@gmail.com", + "San Francisco, CA, USA"), + List.of( + new Link("sofiamartinez.design", "https://sofiamartinez.design"), + new Link("LinkedIn", "https://www.linkedin.com/in/sofia-martinez-ux")), + Optional.empty()); + } + + /** The opening lines, set as written rather than wrapped. */ + private static ParagraphSection summary() { + return new ParagraphSection("SUMMARY", String.join(NEWLINE, + "User-centered designer with 5+ years of experience " + + "crafting intuitive digital experiences for web and " + + "mobile products.", + "I combine research, design thinking, and visual design " + + "to solve complex problems and deliver accessible, " + + "elegant solutions.", + "Passionate about collaborating with cross-functional " + + "teams to create products that delight users and drive " + + "business impact.")); + } + + /** The six-up grid: a mark, a label and a description each. */ + private static EntriesSection skills() { + return new EntriesSection("DESIGN SKILLS", List.of( + CvEntry.builder("UX RESEARCH") + .icon("ux-research") + .body("User interviews, surveys, personas, journey maps, " + + "competitive analysis") + .build(), + CvEntry.builder("INFORMATION ARCHITECTURE") + .icon("information-architecture") + .body("Sitemaps, user flows, card sorting, content strategy") + .build(), + CvEntry.builder("WIREFRAMING") + .icon("wireframing") + .body("Low to high-fidelity wireframes, layout design, " + + "interaction flows") + .build(), + CvEntry.builder("PROTOTYPING") + .icon("prototyping") + .body("Interactive prototypes, microinteractions, transitions") + .build(), + CvEntry.builder("USABILITY TESTING") + .icon("usability-testing") + .body("Test planning, usability tests, heuristic evaluation, " + + "insights & iteration") + .build(), + CvEntry.builder("DESIGN SYSTEMS") + .icon("design-systems") + .body("Component libraries, design tokens, pattern " + + "documentation") + .build())); + } + + /** The tools strip — names without levels. */ + private static SkillsSection tools() { + List entries = new ArrayList<>(); + for (String name : new String[] { + "Figma", "FigJam", "Adobe XD", "Sketch", "Miro", "Notion", "Jira", "Slack", "Maze", "Hotjar", "Airtable"}) { + entries.add(CvSkill.of(name)); + } + return new SkillsSection("TOOLS", + List.of(new SkillGroup("TOOLS", entries))); + } + + /** The roles held, each a headline over its bullets. */ + private static EntriesSection experience() { + return new EntriesSection("EXPERIENCE", List.of( + CvEntry.builder("Senior UX/UI Designer") + .subtitle("NovaFin (FinTech Startup)") + .place("San Francisco, CA") + .date("2022 - Present") + .body(String.join(NEWLINE, + "Led end-to-end design for a personal finance platform, " + + "from discovery to launch, improving task success rate by " + + "32%.", + "Conducted user research, built personas and journey " + + "maps, and translated insights into clear product " + + "opportunities.", + "Designed wireframes, prototypes, and high-fidelity " + + "interfaces for web and mobile using Figma.", + "Established and maintained a scalable design system, " + + "increasing design consistency and development speed.", + "Collaborated with product managers, engineers, and data " + + "analysts in an agile environment.")) + .build(), + CvEntry.builder("UX Designer") + .subtitle("BrightHealth (HealthTech)") + .place("San Francisco, CA") + .date("2020 - 2022") + .body(String.join(NEWLINE, + "Redesigned patient portal experience, reducing support " + + "tickets by 28% and improving user satisfaction.", + "Planned and facilitated usability tests and synthesized " + + "findings to drive iterative improvements.", + "Partnered with engineers to ensure feasible solutions " + + "and pixel-perfect implementation.", + "Created reusable components and style guides to support " + + "a cohesive cross-platform experience.")) + .build(), + CvEntry.builder("Junior UX/UI Designer") + .subtitle("Lumen Digital Agency") + .place("Austin, TX") + .date("2018 - 2020") + .body(String.join(NEWLINE, + "Supported discovery workshops, user flows, and " + + "wireframes for clients across e-commerce and SaaS.", + "Designed responsive websites and landing pages following " + + "best practices in accessibility and usability.", + "Collaborated with copywriters and developers to deliver " + + "high-quality, user-centered solutions.")) + .build())); + } + + /** The projects, each behind its tinted tile. */ + private static EntriesSection projects() { + return new EntriesSection("SELECTED PROJECTS", List.of( + CvEntry.builder("NovaFin Mobile App") + .subtitle("Personal Finance Management") + .date("2023") + .icon("project-wallet") + .body("Led UX/UI design for budgeting, goal tracking, and " + + "insights features. Conducted user interviews and " + + "usability tests that informed a simplified navigation " + + "and data visualization. Resulted in 32% improvement in " + + "task success rate and 22% increase in weekly active " + + "users.") + .build(), + CvEntry.builder("BrightHealth Patient Portal") + .subtitle("Healthcare Platform Redesign") + .date("2021") + .icon("project-health") + .body("Redesigned key patient flows including appointment " + + "scheduling, test results, and messaging. Created a new " + + "design system and component library used across web and " + + "mobile. Reduced support tickets by 28%.") + .build())); + } + + /** The degree, with the place and the years on its third line. */ + private static EntriesSection education() { + return new EntriesSection("EDUCATION", List.of( + CvEntry.builder("Bachelor of Fine Arts in Graphic Design") + .subtitle("California College of the Arts") + .place("San Francisco, CA") + .date("2014 - 2018") + .icon("graduation") + .build())); + } + + /** The languages, each carrying both the rating and the word for it. */ + private static SkillsSection languages() { + return new SkillsSection("LANGUAGES", List.of(new SkillGroup("LANGUAGES", List.of( + CvSkill.of("English", 1.0, "Native"), + CvSkill.of("Spanish", 1.0, "Native"), + CvSkill.of("Portuguese", 0.6, "Conversational"))))); + } + + /** The closing line. Its title names the berth and is not drawn. */ + private static ParagraphSection quote() { + return new ParagraphSection("QUOTE", + "I design with empathy, iterate with purpose, and " + + "collaborate to build products people love."); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/cv/v2/VioletGridExample.java b/examples/src/main/java/com/demcha/examples/templates/cv/v2/VioletGridExample.java new file mode 100644 index 000000000..646ab9a06 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/cv/v2/VioletGridExample.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.VioletGrid; +import com.demcha.examples.support.ExampleOutputPaths; +import com.demcha.examples.support.VioletGridSampleData; + +import java.nio.file.Path; + +/** + * Renders the layered {@code cv.v2} Violet Grid preset against the design + * sample. + * + *

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

+ * + *

The preset owns its page geometry — every length is a share of the + * design's own grid — so the session starts unconfigured.

+ */ +public final class VioletGridExample { + + private VioletGridExample() { + } + + /** + * @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-violet-grid-v2.pdf"); + CvDocument doc = VioletGridSampleData.sample(); + DocumentTemplate template = VioletGrid.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/VioletGridFixtures.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridFixtures.java new file mode 100644 index 000000000..88876cecc --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridFixtures.java @@ -0,0 +1,222 @@ +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; + +/** + * Shared fixture data for the {@link VioletGrid} 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 VioletGridFixtures { + + /** The break a body stacks its lines on. */ + private static final String NEWLINE = String.valueOf((char) 10); + + private VioletGridFixtures() { + } + + /** + * The canonical one-page CV — five contact lines including two links, + * three opening lines, six marked skills, eleven tools, three roles, two + * projects, a degree, three rated languages and the closing quotation. + */ + static CvDocument canonicalCv() { + List placements = new ArrayList<>(); + placements.add(new CvDocument.Placement(Slot.MAIN, summary())); + placements.add(new CvDocument.Placement(Slot.MAIN, skills())); + placements.add(new CvDocument.Placement(Slot.MAIN, tools())); + placements.add(new CvDocument.Placement(Slot.MAIN, experience())); + placements.add(new CvDocument.Placement(Slot.MAIN, projects())); + placements.add(new CvDocument.Placement(Slot.FOOTER, education())); + placements.add(new CvDocument.Placement(Slot.FOOTER, languages())); + placements.add(new CvDocument.Placement(Slot.FOOTER, quote())); + return new CvDocument(identity(), placements); + } + + static CvIdentity identity() { + return new CvIdentity( + CvName.of("SOFIA", "MARTINEZ"), + "UX / UI DESIGNER", + new Contact("+1 (415) 555-7842", + "sofia.martinez.design@gmail.com", + "San Francisco, CA, USA"), + List.of( + new Link("sofiamartinez.design", "https://sofiamartinez.design"), + new Link("LinkedIn", "https://www.linkedin.com/in/sofia-martinez-ux")), + Optional.empty()); + } + + /** The opening lines, set as written rather than wrapped. */ + static ParagraphSection summary() { + return new ParagraphSection("SUMMARY", String.join(NEWLINE, + "User-centered designer with 5+ years of experience " + + "crafting intuitive digital experiences for web and " + + "mobile products.", + "I combine research, design thinking, and visual design " + + "to solve complex problems and deliver accessible, " + + "elegant solutions.", + "Passionate about collaborating with cross-functional " + + "teams to create products that delight users and drive " + + "business impact.")); + } + + /** The six-up grid: a mark, a label and a description each. */ + static EntriesSection skills() { + return new EntriesSection("DESIGN SKILLS", List.of( + CvEntry.builder("UX RESEARCH") + .icon("ux-research") + .body("User interviews, surveys, personas, journey maps, " + + "competitive analysis") + .build(), + CvEntry.builder("INFORMATION ARCHITECTURE") + .icon("information-architecture") + .body("Sitemaps, user flows, card sorting, content strategy") + .build(), + CvEntry.builder("WIREFRAMING") + .icon("wireframing") + .body("Low to high-fidelity wireframes, layout design, " + + "interaction flows") + .build(), + CvEntry.builder("PROTOTYPING") + .icon("prototyping") + .body("Interactive prototypes, microinteractions, transitions") + .build(), + CvEntry.builder("USABILITY TESTING") + .icon("usability-testing") + .body("Test planning, usability tests, heuristic evaluation, " + + "insights & iteration") + .build(), + CvEntry.builder("DESIGN SYSTEMS") + .icon("design-systems") + .body("Component libraries, design tokens, pattern " + + "documentation") + .build())); + } + + /** The tools strip — names without levels. */ + static SkillsSection tools() { + List entries = new ArrayList<>(); + for (String name : new String[] { + "Figma", "FigJam", "Adobe XD", "Sketch", "Miro", "Notion", "Jira", "Slack", "Maze", "Hotjar", "Airtable"}) { + entries.add(CvSkill.of(name)); + } + return new SkillsSection("TOOLS", + List.of(new SkillGroup("TOOLS", entries))); + } + + /** The roles held, each a headline over its bullets. */ + static EntriesSection experience() { + return new EntriesSection("EXPERIENCE", List.of( + CvEntry.builder("Senior UX/UI Designer") + .subtitle("NovaFin (FinTech Startup)") + .place("San Francisco, CA") + .date("2022 - Present") + .body(String.join(NEWLINE, + "Led end-to-end design for a personal finance platform, " + + "from discovery to launch, improving task success rate by " + + "32%.", + "Conducted user research, built personas and journey " + + "maps, and translated insights into clear product " + + "opportunities.", + "Designed wireframes, prototypes, and high-fidelity " + + "interfaces for web and mobile using Figma.", + "Established and maintained a scalable design system, " + + "increasing design consistency and development speed.", + "Collaborated with product managers, engineers, and data " + + "analysts in an agile environment.")) + .build(), + CvEntry.builder("UX Designer") + .subtitle("BrightHealth (HealthTech)") + .place("San Francisco, CA") + .date("2020 - 2022") + .body(String.join(NEWLINE, + "Redesigned patient portal experience, reducing support " + + "tickets by 28% and improving user satisfaction.", + "Planned and facilitated usability tests and synthesized " + + "findings to drive iterative improvements.", + "Partnered with engineers to ensure feasible solutions " + + "and pixel-perfect implementation.", + "Created reusable components and style guides to support " + + "a cohesive cross-platform experience.")) + .build(), + CvEntry.builder("Junior UX/UI Designer") + .subtitle("Lumen Digital Agency") + .place("Austin, TX") + .date("2018 - 2020") + .body(String.join(NEWLINE, + "Supported discovery workshops, user flows, and " + + "wireframes for clients across e-commerce and SaaS.", + "Designed responsive websites and landing pages following " + + "best practices in accessibility and usability.", + "Collaborated with copywriters and developers to deliver " + + "high-quality, user-centered solutions.")) + .build())); + } + + /** The projects, each behind its tinted tile. */ + static EntriesSection projects() { + return new EntriesSection("SELECTED PROJECTS", List.of( + CvEntry.builder("NovaFin Mobile App") + .subtitle("Personal Finance Management") + .date("2023") + .icon("project-wallet") + .body("Led UX/UI design for budgeting, goal tracking, and " + + "insights features. Conducted user interviews and " + + "usability tests that informed a simplified navigation " + + "and data visualization. Resulted in 32% improvement in " + + "task success rate and 22% increase in weekly active " + + "users.") + .build(), + CvEntry.builder("BrightHealth Patient Portal") + .subtitle("Healthcare Platform Redesign") + .date("2021") + .icon("project-health") + .body("Redesigned key patient flows including appointment " + + "scheduling, test results, and messaging. Created a new " + + "design system and component library used across web and " + + "mobile. Reduced support tickets by 28%.") + .build())); + } + + /** The degree, with the place and the years on its third line. */ + static EntriesSection education() { + return new EntriesSection("EDUCATION", List.of( + CvEntry.builder("Bachelor of Fine Arts in Graphic Design") + .subtitle("California College of the Arts") + .place("San Francisco, CA") + .date("2014 - 2018") + .icon("graduation") + .build())); + } + + /** The languages, each carrying both the rating and the word for it. */ + static SkillsSection languages() { + return new SkillsSection("LANGUAGES", List.of(new SkillGroup("LANGUAGES", List.of( + CvSkill.of("English", 1.0, "Native"), + CvSkill.of("Spanish", 1.0, "Native"), + CvSkill.of("Portuguese", 0.6, "Conversational"))))); + } + + /** The closing line. Its title names the berth and is not drawn. */ + static ParagraphSection quote() { + return new ParagraphSection("QUOTE", + "I design with empathy, iterate with purpose, and " + + "collaborate to build products people love."); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridLayoutSnapshotTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridLayoutSnapshotTest.java new file mode 100644 index 000000000..c47638b85 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridLayoutSnapshotTest.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 VioletGrid} — 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 VioletGridLayoutSnapshotTest { + + @Test + void canonicalCvMatchesLayoutSnapshot() throws Exception { + try (DocumentSession session = GraphCompose.document().create()) { + VioletGrid.create().compose(session, VioletGridFixtures.canonicalCv()); + assertThat(session.layoutSnapshot().totalPages()).isEqualTo(1); + TemplateTestSupport.assertCanonicalSnapshot( + session, "violet_grid_layout", "cv-v2"); + } + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridSmokeTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridSmokeTest.java new file mode 100644 index 000000000..f6a36a2cd --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridSmokeTest.java @@ -0,0 +1,266 @@ +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.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 VioletGrid} — proves the preset renders a + * {@link CvDocument} end-to-end with its packaged marks, sets the two-tone + * name from the structured one, draws only the quotation's body, reports an + * unknown mark as a data error, 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. + */ +class VioletGridSmokeTest { + + 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()) { + VioletGrid.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 name and every heading are + * letter-spaced with real space runs, 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 : VioletGridFixtures.canonicalCv().placements()) { + if (placement.section().title().equals(replacedTitle)) { + placements.add(new CvDocument.Placement(slot, replacement)); + } else { + placements.add(placement); + } + } + return new CvDocument(VioletGridFixtures.identity(), placements); + } + + @Test + void exposesStableIdentity() { + DocumentTemplate template = VioletGrid.create(); + assertThat(template.id()).isEqualTo(VioletGrid.ID); + assertThat(template.displayName()).isEqualTo(VioletGrid.DISPLAY_NAME); + } + + @Test + void rendersCanonicalCvWithPackagedMarks() throws Exception { + render(VioletGridFixtures.canonicalCv()); + } + + @Test + void canonicalRenderCarriesEveryBandsText() throws Exception { + String text = textOf(render(VioletGridFixtures.canonicalCv())); + assertThat(text) + .contains("sofia.martinez.design@gmail.com") + .contains("User-centered designer") + .contains("Figma") + .contains("NovaFin (FinTech Startup)") + .contains("NovaFin Mobile App") + .contains("Bachelor of Fine Arts in Graphic Design") + .contains("Conversational") + .contains("I design with empathy"); + assertThat(compact(text)) + .contains("SOFIAMARTINEZ") + .contains("UX/UIDESIGNER") + .contains("DESIGNSKILLS") + .doesNotContain("QUOTE"); + } + + @Test + void theNameIsSetFromTheStructuredNameInTwoTones() throws Exception { + // Both names are runs of one paragraph, so the gap between them is a + // real word space; the two tones are what the design asks for. + String text = textOf(render(VioletGridFixtures.canonicalCv())); + assertThat(text.lines().map(String::strip).toList()) + .anyMatch(line -> line.contains("SOFIA") && line.contains("MARTINEZ")); + } + + @Test + void contactChannelsAndLinksAreClickable() throws Exception { + List targets = linkTargets(render(VioletGridFixtures.canonicalCv())); + assertThat(targets) + .contains("mailto:sofia.martinez.design@gmail.com") + .contains("tel:+14155557842") + .contains("https://sofiamartinez.design") + .contains("https://www.linkedin.com/in/sofia-martinez-ux"); + } + + @Test + void aLinkShowsItsLabelAndHidesItsAddress() throws Exception { + // The column draws the label, so its width is the same whatever the + // profile behind it is called; the address is reachable, not written. + byte[] pdfBytes = render(VioletGridFixtures.canonicalCv()); + assertThat(textOf(pdfBytes)) + .contains("LinkedIn") + .doesNotContain("linkedin.com/in/sofia-martinez-ux"); + assertThat(linkTargets(pdfBytes)) + .contains("https://www.linkedin.com/in/sofia-martinez-ux"); + } + + @Test + void aRoleAProjectAndADegreeBecomeLinksWhenTheirEntryCarriesOne() throws Exception { + List placements = new ArrayList<>(); + for (CvDocument.Placement placement : VioletGridFixtures.canonicalCv().placements()) { + placements.add(switch (placement.section().title()) { + case "EXPERIENCE" -> new CvDocument.Placement(Slot.MAIN, + new EntriesSection("EXPERIENCE", List.of( + CvEntry.builder("Senior UX/UI Designer") + .subtitle("NovaFin") + .place("San Francisco, CA") + .date("2022") + .link("https://example.test/novafin") + .body("Led end-to-end design.") + .build()))); + case "SELECTED PROJECTS" -> new CvDocument.Placement(Slot.MAIN, + new EntriesSection("SELECTED PROJECTS", List.of( + CvEntry.builder("NovaFin Mobile App") + .subtitle("Personal Finance") + .date("2023") + .icon("project-wallet") + .link("https://example.test/app") + .body("Led UX/UI design.") + .build()))); + case "EDUCATION" -> new CvDocument.Placement(Slot.FOOTER, + new EntriesSection("EDUCATION", List.of( + CvEntry.builder("Bachelor of Fine Arts") + .subtitle("California College of the Arts") + .place("San Francisco, CA") + .date("2014 - 2018") + .icon("graduation") + .link("https://example.test/cca") + .build()))); + default -> placement; + }); + } + + List targets = linkTargets( + render(new CvDocument(VioletGridFixtures.identity(), placements))); + assertThat(targets) + .contains("https://example.test/novafin") + .contains("https://example.test/app") + .contains("https://example.test/cca"); + } + + @Test + void anUnknownMarkIsReportedAsADataError() { + EntriesSection wrong = new EntriesSection("DESIGN SKILLS", List.of( + CvEntry.builder("Telepathy").icon("telescope").body("Reading minds.").build())); + + assertThatThrownBy(() -> render(withSection(Slot.MAIN, wrong, "DESIGN SKILLS"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("telescope") + .hasMessageContaining("wireframing"); + } + + @Test + void aSkillWithoutAMarkIsDrawnWithoutOne() throws Exception { + EntriesSection unmarked = new EntriesSection("DESIGN SKILLS", List.of( + CvEntry.builder("UX RESEARCH").body("User interviews and surveys.").build())); + + String text = textOf(render(withSection(Slot.MAIN, unmarked, "DESIGN SKILLS"))); + // The description wraps inside its narrow column, so a fragment + // of one line is what is asserted. + assertThat(text).contains("User interviews and"); + } + + @Test + void aDocumentWithNothingButAnIdentityStillRenders() throws Exception { + // Every berth empty: the name, the discipline line and its rule, and + // nothing else on the sheet. + render(new CvDocument(VioletGridFixtures.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 aLongerCvRunsOntoASecondPageRatherThanLosingARole() throws Exception { + // The page is a stack of bands, so more content than the design holds + // runs over; each entry is held together, so a role is never cut in two. + List many = new ArrayList<>(); + for (int index = 0; index < 8; index++) { + many.add(CvEntry.builder("Senior UX/UI Designer " + index) + .subtitle("NovaFin") + .place("San Francisco, CA") + .date("2022") + .body("Led end-to-end design for a personal finance platform.") + .build()); + } + + try (DocumentSession session = GraphCompose.document().create()) { + VioletGrid.create().compose(session, withSection(Slot.MAIN, + new EntriesSection("EXPERIENCE", many), "EXPERIENCE")); + assertThat(session.layoutSnapshot().totalPages()).isGreaterThan(1); + } + String text = textOf(render(withSection(Slot.MAIN, + new EntriesSection("EXPERIENCE", many), "EXPERIENCE"))); + assertThat(text).contains("Senior UX/UI Designer 7"); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridVisualParityTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridVisualParityTest.java new file mode 100644 index 000000000..9645814b1 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/VioletGridVisualParityTest.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 VioletGrid} — 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 VioletGridVisualParityTest { + + 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()) { + VioletGrid.create().compose(document, VioletGridFixtures.canonicalCv()); + pdfBytes = document.toPdfBytes(); + } + + PdfVisualRegression.standard() + .baselineRoot(BASELINE_ROOT) + .perPixelTolerance(PER_PIXEL_TOLERANCE) + .mismatchedPixelBudget(PIXEL_DIFF_BUDGET) + .assertMatchesBaseline("violet_grid", pdfBytes); + } +} diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/violet_grid_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/violet_grid_layout.json new file mode 100644 index 000000000..34cb04f2c --- /dev/null +++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/violet_grid_layout.json @@ -0,0 +1,4217 @@ +{ + "formatVersion" : "2.0", + "canvas" : { + "pageWidth" : 595.276, + "pageHeight" : 841.89, + "innerWidth" : 535.466, + "innerHeight" : 818.192, + "margin" : { + "top" : 13.542, + "right" : 29.905, + "bottom" : 10.156, + "left" : 29.905 + } + }, + "totalPages" : 1, + "nodes" : [ { + "path" : "VioletGridCv[0]", + "entityName" : "VioletGridCv", + "entityKind" : "ContainerNode", + "parentPath" : null, + "childIndex" : 0, + "depth" : 1, + "layer" : 1, + "computedX" : 29.905, + "computedY" : 10.348, + "placementX" : 29.905, + "placementY" : 10.348, + "placementWidth" : 535.466, + "placementHeight" : 818.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 818.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" : "VioletGridCv[0]/Header[0]", + "entityName" : "Header", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 0, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 734.92, + "placementX" : 29.905, + "placementY" : 734.92, + "placementWidth" : 535.466, + "placementHeight" : 93.428, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 93.428, + "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" : "VioletGridCv[0]/Header[0]/Identity[0]", + "entityName" : "Identity", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/Header[0]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 739.279, + "placementX" : 29.905, + "placementY" : 739.279, + "placementWidth" : 284.374, + "placementHeight" : 89.069, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 284.374, + "contentHeight" : 89.069, + "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" : "VioletGridCv[0]/Header[0]/Identity[0]/Name[0]", + "entityName" : "Name", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Header[0]/Identity[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 29.905, + "computedY" : 775.002, + "placementX" : 29.905, + "placementY" : 775.002, + "placementWidth" : 284.374, + "placementHeight" : 53.346, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 284.374, + "contentHeight" : 53.346, + "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" : "VioletGridCv[0]/Header[0]/Identity[0]/Discipline[1]", + "entityName" : "Discipline", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Header[0]/Identity[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 29.905, + "computedY" : 754.683, + "placementX" : 29.905, + "placementY" : 754.683, + "placementWidth" : 157.277, + "placementHeight" : 20.319, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 157.277, + "contentHeight" : 20.319, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 13.147, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Header[0]/Identity[0]/IdentityRule[2]", + "entityName" : "IdentityRule", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/Header[0]/Identity[0]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 29.905, + "computedY" : 739.279, + "placementX" : 29.905, + "placementY" : 739.279, + "placementWidth" : 39.497, + "placementHeight" : 2.257, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 39.497, + "contentHeight" : 2.257, + "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" : "VioletGridCv[0]/Header[0]/Contact[1]", + "entityName" : "Contact", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/Header[0]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 398.355, + "computedY" : 734.92, + "placementX" : 398.355, + "placementY" : 734.92, + "placementWidth" : 152.21, + "placementHeight" : 93.428, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 152.21, + "contentHeight" : 93.428, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 4.514, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Header[0]/Contact[1]/Contact_0_email[0]", + "entityName" : "Contact_0_email", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Header[0]/Contact[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 398.355, + "computedY" : 812.334, + "placementX" : 398.355, + "placementY" : 812.334, + "placementWidth" : 152.21, + "placementHeight" : 11.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 152.21, + "contentHeight" : 11.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.854, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Header[0]/Contact[1]/Contact_1_phone[1]", + "entityName" : "Contact_1_phone", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Header[0]/Contact[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 398.355, + "computedY" : 792.98, + "placementX" : 398.355, + "placementY" : 792.98, + "placementWidth" : 97.746, + "placementHeight" : 11.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 97.746, + "contentHeight" : 11.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.854, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Header[0]/Contact[1]/Contact_2_location[2]", + "entityName" : "Contact_2_location", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Header[0]/Contact[1]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 398.355, + "computedY" : 773.627, + "placementX" : 398.355, + "placementY" : 773.627, + "placementWidth" : 114.927, + "placementHeight" : 11.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 114.927, + "contentHeight" : 11.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.854, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Header[0]/Contact[1]/Contact_3_website[3]", + "entityName" : "Contact_3_website", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Header[0]/Contact[1]", + "childIndex" : 3, + "depth" : 4, + "layer" : 4, + "computedX" : 398.355, + "computedY" : 754.273, + "placementX" : 398.355, + "placementY" : 754.273, + "placementWidth" : 103.158, + "placementHeight" : 11.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 103.158, + "contentHeight" : 11.5, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 7.854, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Header[0]/Contact[1]/Contact_4_linkedin[4]", + "entityName" : "Contact_4_linkedin", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Header[0]/Contact[1]", + "childIndex" : 4, + "depth" : 4, + "layer" : 4, + "computedX" : 398.355, + "computedY" : 734.92, + "placementX" : 398.355, + "placementY" : 734.92, + "placementWidth" : 56.382, + "placementHeight" : 11.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 56.382, + "contentHeight" : 11.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" : "VioletGridCv[0]/SummaryLine_0[1]", + "entityName" : "SummaryLine_0", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 1, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 709.078, + "placementX" : 29.905, + "placementY" : 709.078, + "placementWidth" : 482.062, + "placementHeight" : 11.172, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 482.062, + "contentHeight" : 11.172, + "margin" : { + "top" : 14.67, + "right" : 0.0, + "bottom" : 1.241, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SummaryLine_1[2]", + "entityName" : "SummaryLine_1", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 2, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 696.664, + "placementX" : 29.905, + "placementY" : 696.664, + "placementWidth" : 495.804, + "placementHeight" : 11.172, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 495.804, + "contentHeight" : 11.172, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 1.241, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SummaryLine_2[3]", + "entityName" : "SummaryLine_2", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 3, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 684.251, + "placementX" : 29.905, + "placementY" : 684.251, + "placementWidth" : 501.018, + "placementHeight" : 11.172, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 501.018, + "contentHeight" : 11.172, + "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" : "VioletGridCv[0]/SkillsHeading[4]", + "entityName" : "SkillsHeading", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 4, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 650.396, + "placementX" : 29.905, + "placementY" : 650.396, + "placementWidth" : 535.466, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 14.783, + "margin" : { + "top" : 19.071, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SkillsHeading[4]/SkillsHeadingText[0]", + "entityName" : "SkillsHeadingText", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillsHeading[4]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 650.396, + "placementX" : 29.905, + "placementY" : 650.396, + "placementWidth" : 76.844, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 76.844, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/SkillsHeading[4]/SkillsHeadingText[0]/SkillsHeadingLabel[0]", + "entityName" : "SkillsHeadingLabel", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillsHeading[4]/SkillsHeadingText[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 29.905, + "computedY" : 650.396, + "placementX" : 29.905, + "placementY" : 650.396, + "placementWidth" : 76.844, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 76.844, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/SkillsHeading[4]/SkillsHeadingRule[1]", + "entityName" : "SkillsHeadingRule", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillsHeading[4]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 106.749, + "computedY" : 656.562, + "placementX" : 106.749, + "placementY" : 656.562, + "placementWidth" : 458.622, + "placementHeight" : 8.617, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 458.622, + "contentHeight" : 8.617, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 7.617, + "right" : 0.0, + "bottom" : 0.0, + "left" : 11.285 + } + }, { + "path" : "VioletGridCv[0]/SkillsHeading[4]/SkillsHeadingRule[1]/SkillsHeadingRuleLine[0]", + "entityName" : "SkillsHeadingRuleLine", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/SkillsHeading[4]/SkillsHeadingRule[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 118.034, + "computedY" : 656.562, + "placementX" : 118.034, + "placementY" : 656.562, + "placementWidth" : 447.337, + "placementHeight" : 1.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 447.337, + "contentHeight" : 1.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" : "VioletGridCv[0]/SkillGrid[5]", + "entityName" : "SkillGrid", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 5, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 562.656, + "placementX" : 29.905, + "placementY" : 562.656, + "placementWidth" : 535.466, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 76.737, + "margin" : { + "top" : 11.003, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]", + "entityName" : "Skill_0", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 569.965, + "placementX" : 29.905, + "placementY" : 569.965, + "placementWidth" : 88.304, + "placementHeight" : 69.429, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 88.304, + "contentHeight" : 69.429, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 6.207, + "bottom" : 0.0, + "left" : 6.207 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]/Align[0]", + "entityName" : null, + "entityKind" : "Align", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 36.112, + "computedY" : 618.893, + "placementX" : 36.112, + "placementY" : 618.893, + "placementWidth" : 75.891, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]/Align[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]/Align[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 63.807, + "computedY" : 618.893, + "placementX" : 63.807, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]/Align[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]/Align[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 63.807, + "computedY" : 618.893, + "placementX" : 63.807, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]/SkillLabel_0[1]", + "entityName" : "SkillLabel_0", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 36.112, + "computedY" : 601.283, + "placementX" : 36.112, + "placementY" : 601.283, + "placementWidth" : 75.891, + "placementHeight" : 7.793, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 7.793, + "margin" : { + "top" : 9.818, + "right" : 0.0, + "bottom" : 3.385, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]/SkillDescription_0[2]", + "entityName" : "SkillDescription_0", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_0[0]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 36.112, + "computedY" : 569.965, + "placementX" : 36.112, + "placementY" : 569.965, + "placementWidth" : 75.891, + "placementHeight" : 27.933, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 27.933, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 1.695, + "bottom" : 0.0, + "left" : 1.695 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_0[1]", + "entityName" : "SkillDivider_0", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 118.209, + "computedY" : 562.656, + "placementX" : 118.209, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_0[1]/SkillDividerRule[0]", + "entityName" : "SkillDividerRule", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_0[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 118.209, + "computedY" : 562.656, + "placementX" : 118.209, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]", + "entityName" : "Skill_1", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 2, + "depth" : 3, + "layer" : 3, + "computedX" : 119.337, + "computedY" : 580.121, + "placementX" : 119.337, + "placementY" : 580.121, + "placementWidth" : 88.304, + "placementHeight" : 59.272, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 88.304, + "contentHeight" : 59.272, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 6.207, + "bottom" : 0.0, + "left" : 6.207 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]/Align[0]", + "entityName" : null, + "entityKind" : "Align", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 125.544, + "computedY" : 618.893, + "placementX" : 125.544, + "placementY" : 618.893, + "placementWidth" : 75.891, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]/Align[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]/Align[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 153.239, + "computedY" : 618.893, + "placementX" : 153.239, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]/Align[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]/Align[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 153.239, + "computedY" : 618.893, + "placementX" : 153.239, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]/SkillLabel_1[1]", + "entityName" : "SkillLabel_1", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 125.544, + "computedY" : 601.283, + "placementX" : 125.544, + "placementY" : 601.283, + "placementWidth" : 75.891, + "placementHeight" : 7.793, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 7.793, + "margin" : { + "top" : 9.818, + "right" : 0.0, + "bottom" : 3.385, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]/SkillDescription_1[2]", + "entityName" : "SkillDescription_1", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_1[2]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 125.544, + "computedY" : 580.121, + "placementX" : 125.544, + "placementY" : 580.121, + "placementWidth" : 75.891, + "placementHeight" : 17.776, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 17.776, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 1.695, + "bottom" : 0.0, + "left" : 1.695 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_1[3]", + "entityName" : "SkillDivider_1", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 3, + "depth" : 3, + "layer" : 3, + "computedX" : 207.641, + "computedY" : 562.656, + "placementX" : 207.641, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_1[3]/SkillDividerRule[0]", + "entityName" : "SkillDividerRule", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_1[3]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 207.641, + "computedY" : 562.656, + "placementX" : 207.641, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]", + "entityName" : "Skill_2", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 4, + "depth" : 3, + "layer" : 3, + "computedX" : 208.77, + "computedY" : 569.965, + "placementX" : 208.77, + "placementY" : 569.965, + "placementWidth" : 88.304, + "placementHeight" : 69.429, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 88.304, + "contentHeight" : 69.429, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 6.207, + "bottom" : 0.0, + "left" : 6.207 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]/Align[0]", + "entityName" : null, + "entityKind" : "Align", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 214.976, + "computedY" : 618.893, + "placementX" : 214.976, + "placementY" : 618.893, + "placementWidth" : 75.891, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]/Align[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]/Align[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 242.672, + "computedY" : 618.893, + "placementX" : 242.672, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]/Align[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]/Align[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 242.672, + "computedY" : 618.893, + "placementX" : 242.672, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]/SkillLabel_2[1]", + "entityName" : "SkillLabel_2", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 214.976, + "computedY" : 601.283, + "placementX" : 214.976, + "placementY" : 601.283, + "placementWidth" : 75.891, + "placementHeight" : 7.793, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 7.793, + "margin" : { + "top" : 9.818, + "right" : 0.0, + "bottom" : 3.385, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]/SkillDescription_2[2]", + "entityName" : "SkillDescription_2", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_2[4]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 214.976, + "computedY" : 569.965, + "placementX" : 214.976, + "placementY" : 569.965, + "placementWidth" : 75.891, + "placementHeight" : 27.933, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 27.933, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 1.695, + "bottom" : 0.0, + "left" : 1.695 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_2[5]", + "entityName" : "SkillDivider_2", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 5, + "depth" : 3, + "layer" : 3, + "computedX" : 297.074, + "computedY" : 562.656, + "placementX" : 297.074, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_2[5]/SkillDividerRule[0]", + "entityName" : "SkillDividerRule", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_2[5]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 297.074, + "computedY" : 562.656, + "placementX" : 297.074, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]", + "entityName" : "Skill_3", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 6, + "depth" : 3, + "layer" : 3, + "computedX" : 298.202, + "computedY" : 569.965, + "placementX" : 298.202, + "placementY" : 569.965, + "placementWidth" : 88.304, + "placementHeight" : 69.429, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 88.304, + "contentHeight" : 69.429, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 6.207, + "bottom" : 0.0, + "left" : 6.207 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]/Align[0]", + "entityName" : null, + "entityKind" : "Align", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 304.409, + "computedY" : 618.893, + "placementX" : 304.409, + "placementY" : 618.893, + "placementWidth" : 75.891, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]/Align[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]/Align[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 332.104, + "computedY" : 618.893, + "placementX" : 332.104, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]/Align[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]/Align[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 332.104, + "computedY" : 618.893, + "placementX" : 332.104, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]/SkillLabel_3[1]", + "entityName" : "SkillLabel_3", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 304.409, + "computedY" : 601.283, + "placementX" : 304.409, + "placementY" : 601.283, + "placementWidth" : 75.891, + "placementHeight" : 7.793, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 7.793, + "margin" : { + "top" : 9.818, + "right" : 0.0, + "bottom" : 3.385, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]/SkillDescription_3[2]", + "entityName" : "SkillDescription_3", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_3[6]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 304.409, + "computedY" : 569.965, + "placementX" : 304.409, + "placementY" : 569.965, + "placementWidth" : 75.891, + "placementHeight" : 27.933, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 27.933, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 1.695, + "bottom" : 0.0, + "left" : 1.695 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_3[7]", + "entityName" : "SkillDivider_3", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 7, + "depth" : 3, + "layer" : 3, + "computedX" : 386.506, + "computedY" : 562.656, + "placementX" : 386.506, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_3[7]/SkillDividerRule[0]", + "entityName" : "SkillDividerRule", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_3[7]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 386.506, + "computedY" : 562.656, + "placementX" : 386.506, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]", + "entityName" : "Skill_4", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 8, + "depth" : 3, + "layer" : 3, + "computedX" : 387.634, + "computedY" : 569.965, + "placementX" : 387.634, + "placementY" : 569.965, + "placementWidth" : 88.304, + "placementHeight" : 69.429, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 88.304, + "contentHeight" : 69.429, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 6.207, + "bottom" : 0.0, + "left" : 6.207 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]/Align[0]", + "entityName" : null, + "entityKind" : "Align", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 393.841, + "computedY" : 618.893, + "placementX" : 393.841, + "placementY" : 618.893, + "placementWidth" : 75.891, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]/Align[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]/Align[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 421.536, + "computedY" : 618.893, + "placementX" : 421.536, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]/Align[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]/Align[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 421.536, + "computedY" : 618.893, + "placementX" : 421.536, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]/SkillLabel_4[1]", + "entityName" : "SkillLabel_4", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 393.841, + "computedY" : 601.283, + "placementX" : 393.841, + "placementY" : 601.283, + "placementWidth" : 75.891, + "placementHeight" : 7.793, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 7.793, + "margin" : { + "top" : 9.818, + "right" : 0.0, + "bottom" : 3.385, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]/SkillDescription_4[2]", + "entityName" : "SkillDescription_4", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_4[8]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 393.841, + "computedY" : 569.965, + "placementX" : 393.841, + "placementY" : 569.965, + "placementWidth" : 75.891, + "placementHeight" : 27.933, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 27.933, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 1.695, + "bottom" : 0.0, + "left" : 1.695 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_4[9]", + "entityName" : "SkillDivider_4", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 9, + "depth" : 3, + "layer" : 3, + "computedX" : 475.938, + "computedY" : 562.656, + "placementX" : 475.938, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_4[9]/SkillDividerRule[0]", + "entityName" : "SkillDividerRule", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/SkillDivider_4[9]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 475.938, + "computedY" : 562.656, + "placementX" : 475.938, + "placementY" : 562.656, + "placementWidth" : 1.128, + "placementHeight" : 76.737, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 1.128, + "contentHeight" : 76.737, + "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" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]", + "entityName" : "Skill_5", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]", + "childIndex" : 10, + "depth" : 3, + "layer" : 3, + "computedX" : 477.067, + "computedY" : 569.965, + "placementX" : 477.067, + "placementY" : 569.965, + "placementWidth" : 88.304, + "placementHeight" : 69.429, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 88.304, + "contentHeight" : 69.429, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 6.207, + "bottom" : 0.0, + "left" : 6.207 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]/Align[0]", + "entityName" : null, + "entityKind" : "Align", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 483.274, + "computedY" : 618.893, + "placementX" : 483.274, + "placementY" : 618.893, + "placementWidth" : 75.891, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]/Align[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]/Align[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 510.969, + "computedY" : 618.893, + "placementX" : 510.969, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]/Align[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]/Align[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 510.969, + "computedY" : 618.893, + "placementX" : 510.969, + "placementY" : 618.893, + "placementWidth" : 20.5, + "placementHeight" : 20.5, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 20.5, + "contentHeight" : 20.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" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]/SkillLabel_5[1]", + "entityName" : "SkillLabel_5", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 483.274, + "computedY" : 601.283, + "placementX" : 483.274, + "placementY" : 601.283, + "placementWidth" : 75.891, + "placementHeight" : 7.793, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 7.793, + "margin" : { + "top" : 9.818, + "right" : 0.0, + "bottom" : 3.385, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]/SkillDescription_5[2]", + "entityName" : "SkillDescription_5", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/SkillGrid[5]/Skill_5[10]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 483.274, + "computedY" : 569.965, + "placementX" : 483.274, + "placementY" : 569.965, + "placementWidth" : 75.891, + "placementHeight" : 27.933, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 75.891, + "contentHeight" : 27.933, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 1.695, + "bottom" : 0.0, + "left" : 1.695 + } + }, { + "path" : "VioletGridCv[0]/ToolsHeading[6]", + "entityName" : "ToolsHeading", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 6, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 532.469, + "placementX" : 29.905, + "placementY" : 532.469, + "placementWidth" : 535.466, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 14.783, + "margin" : { + "top" : 15.404, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/ToolsHeading[6]/ToolsHeadingText[0]", + "entityName" : "ToolsHeadingText", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ToolsHeading[6]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 532.469, + "placementX" : 29.905, + "placementY" : 532.469, + "placementWidth" : 34.064, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 34.064, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/ToolsHeading[6]/ToolsHeadingText[0]/ToolsHeadingLabel[0]", + "entityName" : "ToolsHeadingLabel", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ToolsHeading[6]/ToolsHeadingText[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 29.905, + "computedY" : 532.469, + "placementX" : 29.905, + "placementY" : 532.469, + "placementWidth" : 34.064, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 34.064, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/ToolsHeading[6]/ToolsHeadingRule[1]", + "entityName" : "ToolsHeadingRule", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ToolsHeading[6]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 63.969, + "computedY" : 538.635, + "placementX" : 63.969, + "placementY" : 538.635, + "placementWidth" : 501.402, + "placementHeight" : 8.617, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 501.402, + "contentHeight" : 8.617, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 7.617, + "right" : 0.0, + "bottom" : 0.0, + "left" : 11.285 + } + }, { + "path" : "VioletGridCv[0]/ToolsHeading[6]/ToolsHeadingRule[1]/ToolsHeadingRuleLine[0]", + "entityName" : "ToolsHeadingRuleLine", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/ToolsHeading[6]/ToolsHeadingRule[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 75.254, + "computedY" : 538.635, + "placementX" : 75.254, + "placementY" : 538.635, + "placementWidth" : 490.117, + "placementHeight" : 1.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 490.117, + "contentHeight" : 1.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" : "VioletGridCv[0]/ToolsLine[7]", + "entityName" : "ToolsLine", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 7, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 512.186, + "placementX" : 29.905, + "placementY" : 512.186, + "placementWidth" : 528.641, + "placementHeight" : 9.732, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 528.641, + "contentHeight" : 9.732, + "margin" : { + "top" : 10.551, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/ExperienceHeading[8]", + "entityName" : "ExperienceHeading", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 8, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 477.259, + "placementX" : 29.905, + "placementY" : 477.259, + "placementWidth" : 535.466, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 14.783, + "margin" : { + "top" : 20.143, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/ExperienceHeading[8]/ExperienceHeadingText[0]", + "entityName" : "ExperienceHeadingText", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ExperienceHeading[8]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 477.259, + "placementX" : 29.905, + "placementY" : 477.259, + "placementWidth" : 63.841, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 63.841, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/ExperienceHeading[8]/ExperienceHeadingText[0]/ExperienceHeadingLabel[0]", + "entityName" : "ExperienceHeadingLabel", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ExperienceHeading[8]/ExperienceHeadingText[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 29.905, + "computedY" : 477.259, + "placementX" : 29.905, + "placementY" : 477.259, + "placementWidth" : 63.841, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 63.841, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/ExperienceHeading[8]/ExperienceHeadingRule[1]", + "entityName" : "ExperienceHeadingRule", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ExperienceHeading[8]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 93.746, + "computedY" : 483.425, + "placementX" : 93.746, + "placementY" : 483.425, + "placementWidth" : 471.625, + "placementHeight" : 8.617, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 471.625, + "contentHeight" : 8.617, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 7.617, + "right" : 0.0, + "bottom" : 0.0, + "left" : 11.285 + } + }, { + "path" : "VioletGridCv[0]/ExperienceHeading[8]/ExperienceHeadingRule[1]/ExperienceHeadingRuleLine[0]", + "entityName" : "ExperienceHeadingRuleLine", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/ExperienceHeading[8]/ExperienceHeadingRule[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 105.031, + "computedY" : 483.425, + "placementX" : 105.031, + "placementY" : 483.425, + "placementWidth" : 460.34, + "placementHeight" : 1.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 460.34, + "contentHeight" : 1.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" : "VioletGridCv[0]/ExperienceEntries[9]", + "entityName" : "ExperienceEntries", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 9, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 262.073, + "placementX" : 29.905, + "placementY" : 262.073, + "placementWidth" : 535.456, + "placementHeight" : 204.296, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.456, + "contentHeight" : 204.296, + "margin" : { + "top" : 10.89, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 75.891 + } + }, { + "path" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]", + "entityName" : "ExperienceEntry_0", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 105.796, + "computedY" : 382.189, + "placementX" : 105.796, + "placementY" : 382.189, + "placementWidth" : 459.566, + "placementHeight" : 84.18, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 459.566, + "contentHeight" : 84.18, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 13.542, + "left" : 20.031 + } + }, { + "path" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]/EntryTitle_0[0]", + "entityName" : "EntryTitle_0", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 125.826, + "computedY" : 455.065, + "placementX" : 125.826, + "placementY" : 455.065, + "placementWidth" : 439.535, + "placementHeight" : 11.304, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 11.304, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]/EntryTitle_0[0]/TitleTable_0[0]", + "entityName" : "TitleTable_0", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]/EntryTitle_0[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 125.826, + "computedY" : 455.065, + "placementX" : 125.826, + "placementY" : 455.065, + "placementWidth" : 439.535, + "placementHeight" : 11.304, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 11.304, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]/EntryTitle_0[0]/Period_0[1]", + "entityName" : "Period_0", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]/EntryTitle_0[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 29.905, + "computedY" : 455.515, + "placementX" : 29.905, + "placementY" : 455.515, + "placementWidth" : 57.005, + "placementHeight" : 10.404, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 57.005, + "contentHeight" : 10.404, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]/EntryTitle_0[0]/Marker_0[2]", + "entityName" : "Marker_0", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]/EntryTitle_0[0]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 101.987, + "computedY" : 456.908, + "placementX" : 101.987, + "placementY" : 456.908, + "placementWidth" : 7.617, + "placementHeight" : 7.617, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 7.617, + "contentHeight" : 7.617, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]/Highlights_0[1]", + "entityName" : "Highlights_0", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_0[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 125.826, + "computedY" : 395.731, + "placementX" : 125.826, + "placementY" : 395.731, + "placementWidth" : 439.535, + "placementHeight" : 55.892, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 55.892, + "margin" : { + "top" : 3.442, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]", + "entityName" : "ExperienceEntry_1", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 105.796, + "computedY" : 309.577, + "placementX" : 105.796, + "placementY" : 309.577, + "placementWidth" : 459.566, + "placementHeight" : 72.613, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 459.566, + "contentHeight" : 72.613, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 13.542, + "left" : 20.031 + } + }, { + "path" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]/EntryTitle_1[0]", + "entityName" : "EntryTitle_1", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 125.826, + "computedY" : 370.885, + "placementX" : 125.826, + "placementY" : 370.885, + "placementWidth" : 439.535, + "placementHeight" : 11.304, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 11.304, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]/EntryTitle_1[0]/TitleTable_1[0]", + "entityName" : "TitleTable_1", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]/EntryTitle_1[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 125.826, + "computedY" : 370.885, + "placementX" : 125.826, + "placementY" : 370.885, + "placementWidth" : 439.535, + "placementHeight" : 11.304, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 11.304, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]/EntryTitle_1[0]/Period_1[1]", + "entityName" : "Period_1", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]/EntryTitle_1[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 29.905, + "computedY" : 371.335, + "placementX" : 29.905, + "placementY" : 371.335, + "placementWidth" : 47.893, + "placementHeight" : 10.404, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 47.893, + "contentHeight" : 10.404, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]/EntryTitle_1[0]/Marker_1[2]", + "entityName" : "Marker_1", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]/EntryTitle_1[0]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 101.987, + "computedY" : 372.729, + "placementX" : 101.987, + "placementY" : 372.729, + "placementWidth" : 7.617, + "placementHeight" : 7.617, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 7.617, + "contentHeight" : 7.617, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]/Highlights_1[1]", + "entityName" : "Highlights_1", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_1[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 125.826, + "computedY" : 323.119, + "placementX" : 125.826, + "placementY" : 323.119, + "placementWidth" : 439.535, + "placementHeight" : 44.325, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 44.325, + "margin" : { + "top" : 3.442, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]", + "entityName" : "ExperienceEntry_2", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]", + "childIndex" : 2, + "depth" : 3, + "layer" : 3, + "computedX" : 105.796, + "computedY" : 262.073, + "placementX" : 105.796, + "placementY" : 262.073, + "placementWidth" : 459.566, + "placementHeight" : 47.504, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 459.566, + "contentHeight" : 47.504, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 20.031 + } + }, { + "path" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]/EntryTitle_2[0]", + "entityName" : "EntryTitle_2", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 125.826, + "computedY" : 298.273, + "placementX" : 125.826, + "placementY" : 298.273, + "placementWidth" : 439.535, + "placementHeight" : 11.304, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 11.304, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]/EntryTitle_2[0]/TitleTable_2[0]", + "entityName" : "TitleTable_2", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]/EntryTitle_2[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 125.826, + "computedY" : 298.273, + "placementX" : 125.826, + "placementY" : 298.273, + "placementWidth" : 439.535, + "placementHeight" : 11.304, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 11.304, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]/EntryTitle_2[0]/Period_2[1]", + "entityName" : "Period_2", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]/EntryTitle_2[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 29.905, + "computedY" : 298.723, + "placementX" : 29.905, + "placementY" : 298.723, + "placementWidth" : 47.893, + "placementHeight" : 10.404, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 47.893, + "contentHeight" : 10.404, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]/EntryTitle_2[0]/Marker_2[2]", + "entityName" : "Marker_2", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]/EntryTitle_2[0]", + "childIndex" : 2, + "depth" : 5, + "layer" : 5, + "computedX" : 101.987, + "computedY" : 300.116, + "placementX" : 101.987, + "placementY" : 300.116, + "placementWidth" : 7.617, + "placementHeight" : 7.617, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 7.617, + "contentHeight" : 7.617, + "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" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]/Highlights_2[1]", + "entityName" : "Highlights_2", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/ExperienceEntries[9]/ExperienceEntry_2[2]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 125.826, + "computedY" : 262.073, + "placementX" : 125.826, + "placementY" : 262.073, + "placementWidth" : 439.535, + "placementHeight" : 32.758, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 439.535, + "contentHeight" : 32.758, + "margin" : { + "top" : 3.442, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/ProjectsHeading[10]", + "entityName" : "ProjectsHeading", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 10, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 231.717, + "placementX" : 29.905, + "placementY" : 231.717, + "placementWidth" : 535.466, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 14.783, + "margin" : { + "top" : 15.573, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/ProjectsHeading[10]/ProjectsHeadingText[0]", + "entityName" : "ProjectsHeadingText", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ProjectsHeading[10]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 231.717, + "placementX" : 29.905, + "placementY" : 231.717, + "placementWidth" : 105.864, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 105.864, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/ProjectsHeading[10]/ProjectsHeadingText[0]/ProjectsHeadingLabel[0]", + "entityName" : "ProjectsHeadingLabel", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/ProjectsHeading[10]/ProjectsHeadingText[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 29.905, + "computedY" : 231.717, + "placementX" : 29.905, + "placementY" : 231.717, + "placementWidth" : 105.864, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 105.864, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/ProjectsHeading[10]/ProjectsHeadingRule[1]", + "entityName" : "ProjectsHeadingRule", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/ProjectsHeading[10]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 135.769, + "computedY" : 237.883, + "placementX" : 135.769, + "placementY" : 237.883, + "placementWidth" : 429.602, + "placementHeight" : 8.617, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 429.602, + "contentHeight" : 8.617, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 7.617, + "right" : 0.0, + "bottom" : 0.0, + "left" : 11.285 + } + }, { + "path" : "VioletGridCv[0]/ProjectsHeading[10]/ProjectsHeadingRule[1]/ProjectsHeadingRuleLine[0]", + "entityName" : "ProjectsHeadingRuleLine", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/ProjectsHeading[10]/ProjectsHeadingRule[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 147.054, + "computedY" : 237.883, + "placementX" : 147.054, + "placementY" : 237.883, + "placementWidth" : 418.317, + "placementHeight" : 1.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 418.317, + "contentHeight" : 1.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" : "VioletGridCv[0]/Project_0[11]", + "entityName" : "Project_0", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 11, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 177.452, + "placementX" : 29.905, + "placementY" : 177.452, + "placementWidth" : 535.466, + "placementHeight" : 45.745, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 45.745, + "margin" : { + "top" : 8.52, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]", + "entityName" : "ProjectThumb_0", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/Project_0[11]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 180.314, + "placementX" : 29.905, + "placementY" : 180.314, + "placementWidth" : 56.989, + "placementHeight" : 42.882, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 56.989, + "contentHeight" : 42.882, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 11.849 + } + }, { + "path" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]/ProjectTile_0[0]", + "entityName" : "ProjectTile_0", + "entityKind" : "ShapeContainerNode", + "parentPath" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 41.754, + "computedY" : 180.314, + "placementX" : 41.754, + "placementY" : 180.314, + "placementWidth" : 45.139, + "placementHeight" : 42.882, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 45.139, + "contentHeight" : 42.882, + "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" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]/ProjectTile_0[0]/ProjectGlyph_0[0]", + "entityName" : "ProjectGlyph_0", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]/ProjectTile_0[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 50.324, + "computedY" : 187.755, + "placementX" : 50.324, + "placementY" : 187.755, + "placementWidth" : 28.0, + "placementHeight" : 28.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 28.0, + "contentHeight" : 28.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" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]/ProjectTile_0[0]/ProjectGlyph_0[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]/ProjectTile_0[0]/ProjectGlyph_0[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 50.324, + "computedY" : 187.755, + "placementX" : 50.324, + "placementY" : 187.755, + "placementWidth" : 28.0, + "placementHeight" : 28.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 28.0, + "contentHeight" : 28.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" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]/ProjectTile_0[0]/ProjectGlyph_0[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/Project_0[11]/ProjectThumb_0[0]/ProjectTile_0[0]/ProjectGlyph_0[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 7, + "layer" : 7, + "computedX" : 50.324, + "computedY" : 187.755, + "placementX" : 50.324, + "placementY" : 187.755, + "placementWidth" : 28.0, + "placementHeight" : 28.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 28.0, + "contentHeight" : 28.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" : "VioletGridCv[0]/Project_0[11]/ProjectCopy_0[1]", + "entityName" : "ProjectCopy_0", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/Project_0[11]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 108.052, + "computedY" : 177.452, + "placementX" : 108.052, + "placementY" : 177.452, + "placementWidth" : 457.309, + "placementHeight" : 45.745, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 457.309, + "contentHeight" : 45.745, + "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.466 + } + }, { + "path" : "VioletGridCv[0]/Project_0[11]/ProjectCopy_0[1]/ProjectTitle_0[0]", + "entityName" : "ProjectTitle_0", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/Project_0[11]/ProjectCopy_0[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 127.519, + "computedY" : 211.652, + "placementX" : 127.519, + "placementY" : 211.652, + "placementWidth" : 437.842, + "placementHeight" : 11.544, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 437.842, + "contentHeight" : 11.544, + "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" : "VioletGridCv[0]/Project_0[11]/ProjectCopy_0[1]/ProjectBody_0[1]", + "entityName" : "ProjectBody_0", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Project_0[11]/ProjectCopy_0[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 127.519, + "computedY" : 177.452, + "placementX" : 127.519, + "placementY" : 177.452, + "placementWidth" : 424.042, + "placementHeight" : 32.169, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 424.042, + "contentHeight" : 32.169, + "margin" : { + "top" : 2.031, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Project_1[12]", + "entityName" : "Project_1", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 12, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 124.751, + "placementX" : 29.905, + "placementY" : 124.751, + "placementWidth" : 535.466, + "placementHeight" : 42.882, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 42.882, + "margin" : { + "top" : 9.818, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]", + "entityName" : "ProjectThumb_1", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/Project_1[12]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 124.751, + "placementX" : 29.905, + "placementY" : 124.751, + "placementWidth" : 56.989, + "placementHeight" : 42.882, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 56.989, + "contentHeight" : 42.882, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 11.849 + } + }, { + "path" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]/ProjectTile_1[0]", + "entityName" : "ProjectTile_1", + "entityKind" : "ShapeContainerNode", + "parentPath" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 41.754, + "computedY" : 124.751, + "placementX" : 41.754, + "placementY" : 124.751, + "placementWidth" : 45.139, + "placementHeight" : 42.882, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 45.139, + "contentHeight" : 42.882, + "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" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]/ProjectTile_1[0]/ProjectGlyph_1[0]", + "entityName" : "ProjectGlyph_1", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]/ProjectTile_1[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 50.324, + "computedY" : 132.193, + "placementX" : 50.324, + "placementY" : 132.193, + "placementWidth" : 28.0, + "placementHeight" : 28.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 28.0, + "contentHeight" : 28.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" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]/ProjectTile_1[0]/ProjectGlyph_1[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]/ProjectTile_1[0]/ProjectGlyph_1[0]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 50.324, + "computedY" : 132.193, + "placementX" : 50.324, + "placementY" : 132.193, + "placementWidth" : 28.0, + "placementHeight" : 28.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 28.0, + "contentHeight" : 28.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" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]/ProjectTile_1[0]/ProjectGlyph_1[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/Project_1[12]/ProjectThumb_1[0]/ProjectTile_1[0]/ProjectGlyph_1[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 7, + "layer" : 7, + "computedX" : 50.324, + "computedY" : 132.193, + "placementX" : 50.324, + "placementY" : 132.193, + "placementWidth" : 28.0, + "placementHeight" : 28.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 28.0, + "contentHeight" : 28.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" : "VioletGridCv[0]/Project_1[12]/ProjectCopy_1[1]", + "entityName" : "ProjectCopy_1", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/Project_1[12]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 108.052, + "computedY" : 132.892, + "placementX" : 108.052, + "placementY" : 132.892, + "placementWidth" : 457.309, + "placementHeight" : 34.742, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 457.309, + "contentHeight" : 34.742, + "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.466 + } + }, { + "path" : "VioletGridCv[0]/Project_1[12]/ProjectCopy_1[1]/ProjectTitle_1[0]", + "entityName" : "ProjectTitle_1", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/Project_1[12]/ProjectCopy_1[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 127.519, + "computedY" : 156.09, + "placementX" : 127.519, + "placementY" : 156.09, + "placementWidth" : 437.842, + "placementHeight" : 11.544, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 437.842, + "contentHeight" : 11.544, + "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" : "VioletGridCv[0]/Project_1[12]/ProjectCopy_1[1]/ProjectBody_1[1]", + "entityName" : "ProjectBody_1", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/Project_1[12]/ProjectCopy_1[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 127.519, + "computedY" : 132.892, + "placementX" : 127.519, + "placementY" : 132.892, + "placementWidth" : 424.178, + "placementHeight" : 21.167, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 424.178, + "contentHeight" : 21.167, + "margin" : { + "top" : 2.031, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/CredentialHeadings[13]", + "entityName" : "CredentialHeadings", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 13, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 101.109, + "placementX" : 29.905, + "placementY" : 101.109, + "placementWidth" : 535.466, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 14.783, + "margin" : { + "top" : 8.859, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/CredentialHeadings[13]/EducationHeadingLabel[0]", + "entityName" : "EducationHeadingLabel", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/CredentialHeadings[13]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 101.109, + "placementX" : 29.905, + "placementY" : 101.109, + "placementWidth" : 62.401, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 62.401, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/CredentialHeadings[13]/EducationHeadingRule[1]", + "entityName" : "EducationHeadingRule", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/CredentialHeadings[13]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 103.591, + "computedY" : 107.276, + "placementX" : 103.591, + "placementY" : 107.276, + "placementWidth" : 159.846, + "placementHeight" : 1.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 159.846, + "contentHeight" : 1.0, + "margin" : { + "top" : 7.617, + "right" : 0.0, + "bottom" : 0.0, + "left" : 11.285 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/CredentialHeadings[13]/SpacerNode[2]", + "entityName" : null, + "entityKind" : "SpacerNode", + "parentPath" : "VioletGridCv[0]/CredentialHeadings[13]", + "childIndex" : 2, + "depth" : 3, + "layer" : 3, + "computedX" : 274.722, + "computedY" : 115.893, + "placementX" : 274.722, + "placementY" : 115.893, + "placementWidth" : 39.497, + "placementHeight" : 0.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 39.497, + "contentHeight" : 0.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" : "VioletGridCv[0]/CredentialHeadings[13]/LanguagesHeadingLabel[3]", + "entityName" : "LanguagesHeadingLabel", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/CredentialHeadings[13]", + "childIndex" : 3, + "depth" : 3, + "layer" : 3, + "computedX" : 314.219, + "computedY" : 101.109, + "placementX" : 314.219, + "placementY" : 101.109, + "placementWidth" : 63.291, + "placementHeight" : 14.783, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 63.291, + "contentHeight" : 14.783, + "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" : "VioletGridCv[0]/CredentialHeadings[13]/LanguagesHeadingRule[4]", + "entityName" : "LanguagesHeadingRule", + "entityKind" : "LineNode", + "parentPath" : "VioletGridCv[0]/CredentialHeadings[13]", + "childIndex" : 4, + "depth" : 3, + "layer" : 3, + "computedX" : 388.794, + "computedY" : 107.276, + "placementX" : 388.794, + "placementY" : 107.276, + "placementWidth" : 165.292, + "placementHeight" : 1.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 165.292, + "contentHeight" : 1.0, + "margin" : { + "top" : 7.617, + "right" : 0.0, + "bottom" : 0.0, + "left" : 11.285 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/CredentialBodies[14]", + "entityName" : "CredentialBodies", + "entityKind" : "RowNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 14, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 50.973, + "placementX" : 29.905, + "placementY" : 50.973, + "placementWidth" : 535.466, + "placementHeight" : 42.519, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 535.466, + "contentHeight" : 42.519, + "margin" : { + "top" : 7.617, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]", + "entityName" : "Education", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 29.905, + "computedY" : 52.849, + "placementX" : 29.905, + "placementY" : 52.849, + "placementWidth" : 242.614, + "placementHeight" : 40.643, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 242.614, + "contentHeight" : 40.643, + "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" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]", + "entityName" : "EducationBody", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 29.905, + "computedY" : 52.849, + "placementX" : 29.905, + "placementY" : 52.849, + "placementWidth" : 242.614, + "placementHeight" : 40.643, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 242.614, + "contentHeight" : 40.643, + "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" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]/EducationLines[0]", + "entityName" : "EducationLines", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 81.251, + "computedY" : 52.849, + "placementX" : 81.251, + "placementY" : 52.849, + "placementWidth" : 191.268, + "placementHeight" : 38.33, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 191.268, + "contentHeight" : 38.33, + "margin" : { + "top" : 2.313, + "right" : 0.0, + "bottom" : 0.0, + "left" : 51.346 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]/EducationBadge[1]", + "entityName" : "EducationBadge", + "entityKind" : "ShapeContainerNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 29.905, + "computedY" : 52.867, + "placementX" : 29.905, + "placementY" : 52.867, + "placementWidth" : 40.625, + "placementHeight" : 40.625, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 40.625, + "contentHeight" : 40.625, + "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" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]/EducationBadge[1]/EducationGlyph[0]", + "entityName" : "EducationGlyph", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]/EducationBadge[1]", + "childIndex" : 0, + "depth" : 6, + "layer" : 6, + "computedX" : 37.218, + "computedY" : 60.179, + "placementX" : 37.218, + "placementY" : 60.179, + "placementWidth" : 26.0, + "placementHeight" : 26.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 26.0, + "contentHeight" : 26.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" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]/EducationBadge[1]/EducationGlyph[0]/SvgIcon[0]", + "entityName" : "SvgIcon", + "entityKind" : "LayerStackNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]/EducationBadge[1]/EducationGlyph[0]", + "childIndex" : 0, + "depth" : 7, + "layer" : 7, + "computedX" : 37.218, + "computedY" : 60.179, + "placementX" : 37.218, + "placementY" : 60.179, + "placementWidth" : 26.0, + "placementHeight" : 26.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 26.0, + "contentHeight" : 26.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" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]/EducationBadge[1]/EducationGlyph[0]/SvgIcon[0]/SvgLayer0[0]", + "entityName" : "SvgLayer0", + "entityKind" : "Path", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]/Education[0]/EducationBody[0]/EducationBadge[1]/EducationGlyph[0]/SvgIcon[0]", + "childIndex" : 0, + "depth" : 8, + "layer" : 8, + "computedX" : 37.218, + "computedY" : 60.179, + "placementX" : 37.218, + "placementY" : 60.179, + "placementWidth" : 26.0, + "placementHeight" : 26.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 26.0, + "contentHeight" : 26.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" : "VioletGridCv[0]/CredentialBodies[14]/CredentialGutter[1]", + "entityName" : "CredentialGutter", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 272.529, + "computedY" : 92.492, + "placementX" : 272.529, + "placementY" : 92.492, + "placementWidth" : 39.497, + "placementHeight" : 1.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 39.497, + "contentHeight" : 1.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" : "VioletGridCv[0]/CredentialBodies[14]/CredentialGutter[1]/SpacerNode[0]", + "entityName" : null, + "entityKind" : "SpacerNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]/CredentialGutter[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 272.529, + "computedY" : 92.492, + "placementX" : 272.529, + "placementY" : 92.492, + "placementWidth" : 39.497, + "placementHeight" : 1.0, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 39.497, + "contentHeight" : 1.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" : "VioletGridCv[0]/CredentialBodies[14]/Languages[2]", + "entityName" : "Languages", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]", + "childIndex" : 2, + "depth" : 3, + "layer" : 3, + "computedX" : 312.026, + "computedY" : 50.973, + "placementX" : 312.026, + "placementY" : 50.973, + "placementWidth" : 253.325, + "placementHeight" : 42.519, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 253.325, + "contentHeight" : 42.519, + "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" : "VioletGridCv[0]/CredentialBodies[14]/Languages[2]/LanguageTable[0]", + "entityName" : "LanguageTable", + "entityKind" : "TableNode", + "parentPath" : "VioletGridCv[0]/CredentialBodies[14]/Languages[2]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 312.026, + "computedY" : 50.973, + "placementX" : 312.026, + "placementY" : 50.973, + "placementWidth" : 253.325, + "placementHeight" : 41.221, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 253.325, + "contentHeight" : 41.221, + "margin" : { + "top" : 1.298, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "VioletGridCv[0]/QuoteBand[15]", + "entityName" : "QuoteBand", + "entityKind" : "SectionNode", + "parentPath" : "VioletGridCv[0]", + "childIndex" : 15, + "depth" : 2, + "layer" : 2, + "computedX" : 29.905, + "computedY" : 10.348, + "placementX" : 29.905, + "placementY" : 10.348, + "placementWidth" : 414.689, + "placementHeight" : 27.084, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 414.689, + "contentHeight" : 27.084, + "margin" : { + "top" : 13.542, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 6.492, + "right" : 22.57, + "bottom" : 6.492, + "left" : 22.57 + } + }, { + "path" : "VioletGridCv[0]/QuoteBand[15]/QuoteLine[0]", + "entityName" : "QuoteLine", + "entityKind" : "ParagraphNode", + "parentPath" : "VioletGridCv[0]/QuoteBand[15]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 52.475, + "computedY" : 16.839, + "placementX" : 52.475, + "placementY" : 16.839, + "placementWidth" : 369.55, + "placementHeight" : 14.1, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 369.55, + "contentHeight" : 14.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 + } + } ] +} diff --git a/qa/src/test/resources/visual-baselines/cv-v2-layered/violet_grid-page-0.png b/qa/src/test/resources/visual-baselines/cv-v2-layered/violet_grid-page-0.png new file mode 100644 index 000000000..baa9ae30b Binary files /dev/null and b/qa/src/test/resources/visual-baselines/cv-v2-layered/violet_grid-page-0.png differ diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGrid.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGrid.java new file mode 100644 index 000000000..18b6ca623 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGrid.java @@ -0,0 +1,229 @@ +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.VioletGridStyles.PAGE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PAGE_MARGIN_BOTTOM; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PAGE_MARGIN_TOP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PAGE_MARGIN_X; + +/** + * Violet Grid — a one-page CV in a single column, banded rather than split: a + * two-tone name beside the contact list, three opening lines, a six-up grid of + * marked skills divided by dotted rules, a strip of tools on inline discs, the + * roles held on a dated timeline, the projects behind tinted tiles, education + * and languages side by side, and a tinted band closing on a quotation. + * + *

Every split is local to one band

+ * + *

Unlike a sidebar CV this page has no page-level grid. The masthead splits + * where the contact marks begin, the timeline into dates / rail / content, a + * project into tile / copy, and the credentials into two halves — and none of + * those four splits knows about any of the others. That is why every + * horizontal arrangement here is either a top-level row or a table, and why + * none of them nests: a row cannot sit inside a row's cell.

+ * + *

Three marks that are relationships, not lengths

+ * + *

A section rule starts where its heading ends, so it is a weighted column + * taking what an auto column leaves. The timeline rail is the left border of + * every entry but the last, so consecutive entries butt into one unbroken line + * whose length follows from how many entries there are. A project's hairline + * is the left border of its copy, so it is exactly as tall as the copy. None + * of the three is a measured height, and none has to be re-measured when the + * content changes.

+ * + *

One page, and what happens past it

+ * + *

The page is a stack of bands, so a CV with more content than the design + * holds runs onto a second page rather than losing anything — each experience + * entry is held together, so a role is never cut in half, but the sheet is + * drawn for one page and a longer one will look like a one-page design that + * ran over. {@link TimelineMinimal} is the preset in this package built to + * paginate.

+ * + *

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 seven:

+ * + *
    + *
  • the summary, a {@link ParagraphSection} of one line per line — set as + * written rather than wrapped, because the design's own breaks leave + * room no greedy wrap would;
  • + *
  • skills, an {@link EntriesSection} whose entries carry a mark, a label + * and a description — the six-up grid;
  • + *
  • tools, a {@link SkillsSection} of unlevelled names — the strip;
  • + *
  • experience and projects, {@link EntriesSection}s;
  • + *
  • education, an {@link EntriesSection} whose first entry is drawn, and + * languages, a {@link SkillsSection} — the closing band;
  • + *
  • the quotation, a {@link ParagraphSection} whose body is the line in + * the tinted band. Only its body is drawn: the title is how the document + * names the berth.
  • + *
+ * + *

A section whose title matches no berth is not drawn, and a berth with no + * section takes its heading and rule with it.

+ * + *

The marks, the ratings and the links

+ * + *

A skill, a project and the degree each take the mark its entry names in + * {@code CvEntry.icon()}, from this preset's own vocabulary, and an unknown + * token is reported as a data error naming the set. An entry with no token is + * drawn without a mark.

+ * + *

A language draws both channels of its skill: the discs from + * {@code CvSkill.level()} and the words beside them from + * {@code CvSkill.note()}.

+ * + *

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, so the contact column's + * width does not depend on how long an address happens to be, and it takes the + * mark of the network it points at or the globe. A role, a project title and + * the degree become links when their entry carries {@code CvEntry.link()}.

+ * + *

Fonts

+ * + *

The sheet is set in Lato, with Asap Condensed for the name, the headings + * and the skill labels. The templates artifact carries neither — 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 = VioletGrid.create();
+ * template.compose(session, cv);
+ * }
+ * + * @since 2.2.3 + */ +public final class VioletGrid { + + /** Stable identifier of this preset. */ + public static final String ID = "violet-grid"; + + /** Human-readable name of this preset. */ + public static final String DISPLAY_NAME = "Violet Grid"; + + private static final List SUMMARY_KEYS = + List.of("summary", "profile", "about"); + private static final List QUOTE_KEYS = + List.of("quote", "motto", "tagline", "closing"); + private static final List SKILL_KEYS = + List.of("skills", "expertise", "competencies"); + private static final List TOOL_KEYS = + List.of("tools", "stack", "software"); + private static final List LANGUAGE_KEYS = + List.of("languages", "language"); + private static final List EXPERIENCE_KEYS = + List.of("experience", "employment", "work history", "career"); + private static final List PROJECT_KEYS = + List.of("projects", "project", "portfolio", "selected work"); + private static final List EDUCATION_KEYS = + List.of("education", "qualifications"); + + private VioletGrid() { + } + + /** + * 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(); + + // The quotation is claimed before the summary: its own keys are + // narrow, but the summary's are broad enough to take a closing line + // titled loosely. + ParagraphSection quote = berth(sections, ParagraphSection.class, QUOTE_KEYS); + ParagraphSection summary = + berthExcept(sections, ParagraphSection.class, SUMMARY_KEYS, quote); + EntriesSection skills = berth(sections, EntriesSection.class, SKILL_KEYS); + SkillsSection tools = berth(sections, SkillsSection.class, TOOL_KEYS); + SkillsSection languages = + berthExcept(sections, SkillsSection.class, LANGUAGE_KEYS, tools); + EntriesSection experience = berth(sections, EntriesSection.class, EXPERIENCE_KEYS); + EntriesSection projects = + berthExcept(sections, EntriesSection.class, PROJECT_KEYS, skills); + EntriesSection education = berth(sections, EntriesSection.class, EDUCATION_KEYS); + + // Every length on the sheet is a share of the design's own grid, so + // the preset sets the page and its margins rather than following + // what the caller configured. + document.pageSize(PAGE).margin(new DocumentInsets( + PAGE_MARGIN_TOP, PAGE_MARGIN_X, PAGE_MARGIN_BOTTOM, PAGE_MARGIN_X)); + document.pageFlow(page -> { + page.name("VioletGridCv"); + page.spacing(0); + VioletGridMasthead.render(page, doc.identity(), summary); + VioletGridBody.renderSkills(page, skills); + VioletGridBody.renderTools(page, tools); + VioletGridBody.renderExperience(page, experience); + VioletGridClosing.renderProjects(page, projects); + VioletGridClosing.renderCredentials(page, education, languages); + VioletGridClosing.renderQuote(page, quote); + }); + } + + /** + * 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/VioletGridBody.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridBody.java new file mode 100644 index 000000000..ebe330d8f --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridBody.java @@ -0,0 +1,342 @@ +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.TableBuilder; +import com.demcha.compose.document.node.DocumentLinkOptions; +import com.demcha.compose.document.node.DocumentNode; +import com.demcha.compose.document.node.HorizontalAlign; +import com.demcha.compose.document.node.LayerAlign; +import com.demcha.compose.document.node.TextAlign; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.table.DocumentTableCell; +import com.demcha.compose.document.table.DocumentTableColumn; +import com.demcha.compose.document.table.DocumentTableTextAnchor; +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.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.VioletGridStyles.ACCENT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BODY; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BODY_FONT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BULLET_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BULLET_DOT_DIAMETER; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BULLET_PITCH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.CONTENT_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DATE_OFFSET; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DISPLAY_FONT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.EMPLOYER_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ENTRY_GAP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ENTRY_INDENT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ENTRY_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TO_ENTRIES; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TO_SKILLS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TO_TOOLS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HIGHLIGHT_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ICON_TO_LABEL; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.INK; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LABEL_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LABEL_TO_DESC; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LINE_FACTOR; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LOCATION_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LOCATION_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.MARKER_DIAMETER; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.MARKER_OFFSET; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.MUTED; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PERIOD_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PIPE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.RAIL_INDENT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.RAIL_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ROLE_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ROLE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.RULE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILLS_TO_TOOLS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILL_COLUMN_INSET; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILL_DASH_OFF; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILL_DASH_ON; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILL_DESC_MEASURE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILL_DESC_PITCH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILL_DESC_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILL_DIVIDER_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SKILL_RAIL_HEIGHT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SUMMARY_TO_SKILLS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.TITLE_TO_BULLETS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.TOOLS_TO_EXPERIENCE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.TOOL_DOT_DIAMETER; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.TOOL_GAP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.TOOL_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.cellStyle; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.centred; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.gap; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.leading; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.style; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.headingRow; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.lines; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.markerDot; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.text; + +/** + * The middle of the sheet: the six-up skills grid, the tools strip and the + * dated timeline. + */ +final class VioletGridBody { + + private VioletGridBody() { + } + + // -- the skills grid --------------------------------------------------- + + /** + * The skills grid — one row with 2n−1 columns: n equal content columns + * interleaved with n−1 fixed hairline columns. + * + *

Building the separators as cells of the same row as the content is + * what stops them drifting from the boundaries they mark, and dividing by + * the entry count is what lets a seventh skill be a data change.

+ */ + static void renderSkills(PageFlowBuilder page, EntriesSection skills) { + if (skills == null || skills.entries().isEmpty()) { + return; + } + page.add(headingRow("Skills", skills.title(), SUMMARY_TO_SKILLS)); + List entries = skills.entries(); + int count = entries.size(); + double columnWidth = (CONTENT_WIDTH - (count - 1) * SKILL_DIVIDER_THICKNESS) / count; + double innerWidth = columnWidth - 2.0 * SKILL_COLUMN_INSET; + double descriptionInset = Math.max(0.0, (innerWidth - SKILL_DESC_MEASURE) / 2.0); + page.addRow("SkillGrid", row -> { + row.spacing(0); + row.margin(new DocumentInsets(HEADING_TO_SKILLS, 0, 0, 0)); + DocumentRowColumn[] columns = new DocumentRowColumn[2 * count - 1]; + for (int index = 0; index < count; index++) { + columns[2 * index] = DocumentRowColumn.weight(1.0); + if (index < count - 1) { + columns[2 * index + 1] = DocumentRowColumn.fixed(SKILL_DIVIDER_THICKNESS); + } + } + row.columns(columns); + for (int index = 0; index < count; index++) { + CvEntry entry = entries.get(index); + int position = index; + row.addSection("Skill_" + position, + column -> renderSkillColumn(column, entry, position, descriptionInset)); + if (index < count - 1) { + row.addSection("SkillDivider_" + position, VioletGridBody::renderDivider); + } + } + }); + } + + /** One column: the mark, the label and the description, on its own axis. */ + private static void renderSkillColumn(SectionBuilder column, CvEntry entry, int index, + double descriptionInset) { + column.spacing(0); + column.padding(0f, (float) SKILL_COLUMN_INSET, 0f, (float) SKILL_COLUMN_INSET); + if (!entry.icon().isBlank()) { + column.addSvgIcon(VioletGridIcons.icon(entry.icon()), + VioletGridIcons.size(entry.icon()), HorizontalAlign.CENTER); + } + column.addParagraph(p -> p + .name("SkillLabel_" + index) + .text(entry.title()) + .align(TextAlign.CENTER) + .lineSpacing(0) + .textStyle(style(DISPLAY_FONT, LABEL_SIZE, INK, true)) + .margin((float) ICON_TO_LABEL, 0f, (float) LABEL_TO_DESC, 0f)); + column.addParagraph(p -> p + .name("SkillDescription_" + index) + .text(entry.body()) + .align(TextAlign.CENTER) + .lineSpacing(leading(SKILL_DESC_PITCH, SKILL_DESC_SIZE)) + .textStyle(style(BODY_FONT, SKILL_DESC_SIZE, MUTED, false)) + // The description wraps to a narrower measure than the label + // above it, which is why the inset is on this paragraph and not + // on the column. + .padding(0f, (float) descriptionInset, 0f, (float) descriptionInset)); + } + + /** + * The dotted rule between two columns. + * + *

A dashed line is the only primitive that produces a dotted rule. An + * accent border would take its height from the column beside it, which is + * what the height constant has to state by hand instead — the trade the + * design's dotted rule costs.

+ */ + private static void renderDivider(SectionBuilder cell) { + cell.spacing(0); + cell.addLine(line -> line + .name("SkillDividerRule") + .vertical(SKILL_RAIL_HEIGHT) + .thickness(SKILL_DIVIDER_THICKNESS) + .color(RULE) + .dashed(SKILL_DASH_ON, SKILL_DASH_OFF)); + } + + // -- the tools strip --------------------------------------------------- + + /** + * The tools — one paragraph of names and inline discs. + * + *

The discs are inline shape runs rather than a bullet glyph, so they + * cannot fall victim to whatever a fallback font ships, and the line is + * running text rather than a grid — which is what the design's constant gap + * either side of every disc says it is.

+ */ + static void renderTools(PageFlowBuilder page, SkillsSection tools) { + if (tools == null || tools.groups().stream().allMatch(g -> g.entries().isEmpty())) { + return; + } + page.add(headingRow("Tools", tools.title(), SKILLS_TO_TOOLS)); + DocumentTextStyle name = style(BODY_FONT, TOOL_SIZE, BODY, false); + List items = new ArrayList<>(); + for (SkillGroup group : tools.groups()) { + for (CvSkill skill : group.entries()) { + items.add(skill.name()); + } + } + page.addParagraph(p -> { + p.name("ToolsLine"); + p.lineSpacing(0); + p.textStyle(name); + for (int index = 0; index < items.size(); index++) { + if (index > 0) { + p.inlineText(TOOL_GAP, name); + p.dot(TOOL_DOT_DIAMETER, ACCENT); + p.inlineText(TOOL_GAP, name); + } + p.inlineText(items.get(index), name); + } + p.margin((float) HEADING_TO_TOOLS, 0f, 0f, 0f); + }); + } + + // -- the timeline ------------------------------------------------------ + + /** + * The dated timeline. + * + *

The entries host is indented so its LEFT EDGE lands on the rail. That + * is what lets each entry's left border BE the rail: consecutive entries + * butt together into one unbroken line, and it stops at the last marker + * because the last entry does not carry it.

+ */ + static void renderExperience(PageFlowBuilder page, EntriesSection experience) { + if (experience == null || experience.entries().isEmpty()) { + return; + } + page.add(headingRow("Experience", experience.title(), TOOLS_TO_EXPERIENCE)); + List entries = experience.entries(); + page.addSection("ExperienceEntries", host -> { + host.spacing(0); + host.margin((float) HEADING_TO_ENTRIES, 0f, 0f, 0f); + host.padding(0f, 0f, 0f, (float) RAIL_INDENT); + for (int i = 0; i < entries.size(); i++) { + CvEntry entry = entries.get(i); + boolean last = i == entries.size() - 1; + int index = i; + host.addSection("ExperienceEntry_" + index, body -> { + body.spacing(0); + body.keepTogether(); + if (!last) { + body.accentLeft(RULE, RAIL_THICKNESS); + } + body.padding(0f, 0f, last ? 0f : (float) ENTRY_GAP, (float) ENTRY_INDENT); + renderEntry(body, entry, index); + }); + } + }); + } + + /** + * One entry: the title line with its dates and marker, then the bullets. + * + *

The dates and the disc ride the title line's own layer stack at + * negative offsets, so both are placed relative to the line they belong to + * rather than at a measured y. The title itself is a two-column table — a + * row here would be a row inside a row's cell.

+ */ + private static void renderEntry(SectionBuilder body, CvEntry entry, int index) { + body.addLayerStack(stack -> stack + .name("EntryTitle_" + index) + .layer(titleLine(entry, index), LayerAlign.TOP_LEFT, 0) + .position(text("Period_" + index, entry.date(), + style(BODY_FONT, PERIOD_SIZE, ACCENT, false)), + DATE_OFFSET, centred(PERIOD_SIZE * LINE_FACTOR), + LayerAlign.TOP_LEFT, 1) + .position(markerDot("Marker_" + index, MARKER_DIAMETER, + MARKER_DIAMETER / LINE_FACTOR, ACCENT), + MARKER_OFFSET, centred(MARKER_DIAMETER), LayerAlign.TOP_LEFT, 1)); + List highlights = lines(entry.body()); + if (highlights.isEmpty()) { + return; + } + body.addTable(table -> { + table.name("Highlights_" + index); + table.margin(new DocumentInsets(TITLE_TO_BULLETS, 0, 0, 0)); + table.width(ENTRY_WIDTH); + table.columns( + DocumentTableColumn.fixed(BULLET_COLUMN), + DocumentTableColumn.fixed(ENTRY_WIDTH - BULLET_COLUMN)); + table.defaultCellStyle(cellStyle( + new DocumentInsets(0, 0, + gap(BULLET_PITCH, HIGHLIGHT_SIZE * LINE_FACTOR), 0), + DocumentTableTextAnchor.TOP_LEFT)); + for (int i = 0; i < highlights.size(); i++) { + table.rowCells( + DocumentTableCell.node(markerDot("HighlightDot_" + index + "_" + i, + BULLET_DOT_DIAMETER, HIGHLIGHT_SIZE, ACCENT)), + DocumentTableCell.node(text("Highlight_" + index + "_" + i, + highlights.get(i), + style(BODY_FONT, HIGHLIGHT_SIZE, BODY, false)))); + } + // The pitch is a gap BETWEEN bullets; left on the last one it + // becomes a gap the entry below already owns. + table.rowStyle(highlights.size() - 1, + cellStyle(DocumentInsets.zero(), DocumentTableTextAnchor.TOP_LEFT)); + }); + } + + /** The role, the employer and the location, as one line of three runs. */ + private static DocumentNode titleLine(CvEntry entry, int index) { + DocumentTextStyle role = style(BODY_FONT, ROLE_SIZE, INK, true); + DocumentTextStyle employer = style(BODY_FONT, EMPLOYER_SIZE, MUTED, false); + ParagraphBuilder title = new ParagraphBuilder() + .name("Role_" + index) + .lineSpacing(0) + .textStyle(role) + .inlineText(entry.title(), role); + if (!entry.subtitle().isBlank()) { + title.inlineText(PIPE, employer).inlineText(entry.subtitle(), employer); + } + if (!entry.link().isBlank()) { + title.link(new DocumentLinkOptions(entry.link())); + } + return new TableBuilder() + .name("TitleTable_" + index) + .width(ENTRY_WIDTH) + .columns( + DocumentTableColumn.fixed(ROLE_COLUMN), + DocumentTableColumn.fixed(LOCATION_COLUMN)) + .defaultCellStyle(cellStyle(DocumentInsets.zero(), + DocumentTableTextAnchor.TOP_LEFT)) + .rowCells( + DocumentTableCell.node(title.build()), + DocumentTableCell.node(new ParagraphBuilder() + .name("Location_" + index) + .text(entry.place()) + .align(TextAlign.RIGHT) + .lineSpacing(0) + .textStyle(style(BODY_FONT, LOCATION_SIZE, MUTED, false)) + .build())) + .build(); + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridClosing.java new file mode 100644 index 000000000..56dfceb63 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridClosing.java @@ -0,0 +1,466 @@ +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.ShapeContainerBuilder; +import com.demcha.compose.document.dsl.TableBuilder; +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.TextAlign; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.table.DocumentTableCell; +import com.demcha.compose.document.table.DocumentTableColumn; +import com.demcha.compose.document.table.DocumentTableTextAnchor; +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.VioletGridStyles.ACCENT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ACCENT_RULE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BADGE_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BADGE_DIAMETER; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BODY; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BODY_FONT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.CREDENTIALS_TO_QUOTE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.CREDENTIAL_GUTTER; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DEGREE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DEGREE_TO_INSTITUTION; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DISPLAY_FONT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.EDUCATION_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.EDUCATION_RULE_WEIGHT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.EDUCATION_TEXT_TOP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.EDU_DETAIL_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.EXPERIENCE_TO_PROJECTS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_RULE_OFFSET; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TO_EDUCATION; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TO_PROJECTS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TO_RULE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TRACKING; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.INK; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.INSTITUTION_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.INSTITUTION_TO_DETAIL; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LANGUAGES_BODY_OFFSET; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LANGUAGES_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LANGUAGES_RULE_WEIGHT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LANGUAGE_LEVEL_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LANGUAGE_NAME_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LANGUAGE_PITCH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LANGUAGE_RATING_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LANGUAGE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LEVEL_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LINE_FACTOR; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.MUTED; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PIPE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECTS_TO_CREDENTIALS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_BODY_PITCH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_BODY_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_COPY_INDENT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_COPY_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_GAP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_SUB_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_TITLE_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_TITLE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_YEAR_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.PROJECT_YEAR_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.QUOTE_GAP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.QUOTE_LINE_BOX; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.QUOTE_PAD_H; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.QUOTE_PAD_V; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.QUOTE_RADIUS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.QUOTE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.RATING_DOT_DIAMETER; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.RATING_EMPTY; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.RATING_GAP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.RATING_SCALE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.RULE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.TABLE_SLACK; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.THUMB_COLUMN; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.THUMB_HEIGHT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.THUMB_PAD_LEFT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.THUMB_RADIUS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.THUMB_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.TINT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.TITLE_TO_BODY; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.cellStyle; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.gap; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.leading; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.style; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.headingRow; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.inlineIcon; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.text; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.tracked; + +/** + * What closes the sheet: the projects, the credentials band and the quote. + */ +final class VioletGridClosing { + + private VioletGridClosing() { + } + + // -- the projects ------------------------------------------------------ + + /** One top-level row per project: the tile, then the copy beside it. */ + static void renderProjects(PageFlowBuilder page, EntriesSection projects) { + if (projects == null || projects.entries().isEmpty()) { + return; + } + page.add(headingRow("Projects", projects.title(), EXPERIENCE_TO_PROJECTS)); + List entries = projects.entries(); + for (int i = 0; i < entries.size(); i++) { + CvEntry entry = entries.get(i); + int index = i; + double top = i == 0 ? HEADING_TO_PROJECTS : PROJECT_GAP; + page.addRow("Project_" + index, row -> { + row.spacing(0); + row.margin(new DocumentInsets(top, 0, 0, 0)); + row.columns( + DocumentRowColumn.fixed(THUMB_COLUMN), + DocumentRowColumn.weight(1.0)); + row.addSection("ProjectThumb_" + index, + cell -> renderThumb(cell, entry, index)); + row.addSection("ProjectCopy_" + index, + cell -> renderProjectCopy(cell, entry, index)); + }); + } + } + + /** + * The tinted tile and the glyph it owns — a shape container rather than a + * fill with a mark beside it: the tile IS the thumbnail, and centring the + * glyph in the container is what keeps the two together when either size + * changes. + */ + private static void renderThumb(SectionBuilder cell, CvEntry entry, int index) { + cell.spacing(0); + cell.padding(0f, 0f, 0f, (float) THUMB_PAD_LEFT); + if (entry.icon().isBlank()) { + return; + } + SectionBuilder glyph = new SectionBuilder(); + glyph.name("ProjectGlyph_" + index); + glyph.addSvgIcon(VioletGridIcons.icon(entry.icon()), VioletGridIcons.size(entry.icon())); + cell.addContainer(tile -> tile + .name("ProjectTile_" + index) + .roundedRect(THUMB_WIDTH, THUMB_HEIGHT, THUMB_RADIUS) + .fillColor(TINT) + .center(glyph.build())); + } + + /** + * The copy beside a thumbnail. The hairline is this section's own left + * border, so it runs the copy's height — which is what the design shows, + * and what a line with a measured height would get wrong the first time + * the copy ran to three lines. + */ + private static void renderProjectCopy(SectionBuilder cell, CvEntry entry, int index) { + DocumentTextStyle title = style(BODY_FONT, PROJECT_TITLE_SIZE, INK, true); + DocumentTextStyle subtitle = style(BODY_FONT, PROJECT_SUB_SIZE, MUTED, false); + cell.spacing(0); + cell.accentLeft(RULE, PROJECT_RULE_THICKNESS); + cell.padding(0f, 0f, 0f, (float) PROJECT_COPY_INDENT); + ParagraphBuilder name = new ParagraphBuilder() + .name("ProjectName_" + index) + .lineSpacing(0) + .textStyle(title) + .inlineText(entry.title(), title); + if (!entry.subtitle().isBlank()) { + name.inlineText(PIPE, subtitle).inlineText(entry.subtitle(), subtitle); + } + if (!entry.link().isBlank()) { + name.link(new DocumentLinkOptions(entry.link())); + } + cell.addTable(table -> table + .name("ProjectTitle_" + index) + .width(PROJECT_COPY_WIDTH) + .columns( + DocumentTableColumn.fixed(PROJECT_TITLE_COLUMN), + DocumentTableColumn.fixed(PROJECT_YEAR_COLUMN)) + .defaultCellStyle(cellStyle(DocumentInsets.zero(), + DocumentTableTextAnchor.TOP_LEFT)) + .rowCells( + DocumentTableCell.node(name.build()), + DocumentTableCell.node(new ParagraphBuilder() + .name("ProjectYear_" + index) + .text(entry.date()) + .align(TextAlign.RIGHT) + .lineSpacing(0) + .textStyle(style(BODY_FONT, PROJECT_YEAR_SIZE, MUTED, false)) + .build()))); + cell.addParagraph(p -> p + .name("ProjectBody_" + index) + .text(entry.body().replace(String.valueOf((char) 10), " ")) + .lineSpacing(leading(PROJECT_BODY_PITCH, PROJECT_BODY_SIZE)) + .textStyle(style(BODY_FONT, PROJECT_BODY_SIZE, BODY, false)) + .margin((float) TITLE_TO_BODY, 0f, 0f, 0f)); + } + + // -- the credentials band ---------------------------------------------- + + /** + * The credentials band: education and languages side by side. + * + *

TWO top-level rows rather than one, and that is not a stylistic + * choice. The obvious shape — one row whose two cells each hold a heading + * and a body — cannot be built: a heading is a text and a rule side by + * side, so a cell holding one would need a nested row, which the layout + * compiler refuses. So the band is flattened: one row carries both headings + * and both rules as direct children, and a second carries the two bodies. + * Nothing is nested, and the two rows share the same column arithmetic, so + * the halves cannot drift apart.

+ */ + static void renderCredentials(PageFlowBuilder page, EntriesSection education, + SkillsSection languages) { + boolean hasEducation = education != null && !education.entries().isEmpty(); + boolean hasLanguages = languages != null + && languages.groups().stream().anyMatch(group -> !group.entries().isEmpty()); + if (!hasEducation && !hasLanguages) { + return; + } + renderCredentialHeadings(page, + hasEducation ? education.title() : "", + hasLanguages ? languages.title() : ""); + page.addRow("CredentialBodies", row -> { + row.spacing(0); + row.margin(new DocumentInsets(HEADING_TO_EDUCATION, 0, 0, 0)); + row.columns( + DocumentRowColumn.fixed(EDUCATION_COLUMN), + DocumentRowColumn.fixed(CREDENTIAL_GUTTER), + DocumentRowColumn.weight(1.0)); + row.addSection("Education", cell -> renderEducation(cell, education)); + row.addSection("CredentialGutter", cell -> cell.spacer(CREDENTIAL_GUTTER, 1.0)); + row.addSection("Languages", cell -> renderLanguages(cell, languages)); + }); + } + + /** + * Both headings and both rules, as children of one row. The two text + * columns take what their words need and the two weighted ones split what + * is left in the ratio the design's rules measure, so each rule still + * begins where its own heading ends. + */ + private static void renderCredentialHeadings(PageFlowBuilder page, String educationTitle, + String languagesTitle) { + page.addRow("CredentialHeadings", row -> { + row.spacing(0); + row.margin(new DocumentInsets(PROJECTS_TO_CREDENTIALS, 0, 0, 0)); + row.columns( + DocumentRowColumn.auto(), + DocumentRowColumn.weight(EDUCATION_RULE_WEIGHT), + DocumentRowColumn.fixed(CREDENTIAL_GUTTER), + DocumentRowColumn.auto(), + DocumentRowColumn.weight(LANGUAGES_RULE_WEIGHT)); + row.add(tracked("EducationHeadingLabel", educationTitle, DISPLAY_FONT, + HEADING_SIZE, ACCENT, true, HEADING_TRACKING).build()); + row.addLine(line -> line + .name("EducationHeadingRule") + .fill() + .thickness(HEADING_RULE_THICKNESS) + .color(ACCENT_RULE) + .margin(new DocumentInsets(HEADING_RULE_OFFSET, 0, 0, HEADING_TO_RULE))); + row.addSpacer(CREDENTIAL_GUTTER); + row.add(tracked("LanguagesHeadingLabel", languagesTitle, DISPLAY_FONT, + HEADING_SIZE, ACCENT, true, HEADING_TRACKING).build()); + row.addLine(line -> line + .name("LanguagesHeadingRule") + .fill() + .thickness(HEADING_RULE_THICKNESS) + .color(ACCENT_RULE) + .margin(new DocumentInsets(HEADING_RULE_OFFSET, 0, 0, HEADING_TO_RULE))); + }); + } + + /** + * The badge beside the degree, the institution and the detail line. + * + *

A layer stack rather than a table: the badge is a shape container and + * the three lines are a stack, and neither survives a table cell here. The + * lines carry a left margin the width of the badge column, so the two + * layers do not overlap and the badge needs no offset.

+ */ + private static void renderEducation(SectionBuilder cell, EntriesSection education) { + cell.spacing(0); + if (education == null || education.entries().isEmpty()) { + return; + } + CvEntry entry = education.entries().get(0); + cell.addLayerStack(stack -> { + stack.name("EducationBody"); + stack.layer(educationLines(entry), LayerAlign.TOP_LEFT, 0); + if (!entry.icon().isBlank()) { + stack.position(badge(entry), 0.0, 0.0, LayerAlign.TOP_LEFT, 1); + } + }); + } + + /** The tinted circle and the glyph it owns. */ + private static DocumentNode badge(CvEntry entry) { + SectionBuilder glyph = new SectionBuilder(); + glyph.name("EducationGlyph"); + glyph.addSvgIcon(VioletGridIcons.icon(entry.icon()), VioletGridIcons.size(entry.icon())); + return new ShapeContainerBuilder() + .name("EducationBadge") + .circle(BADGE_DIAMETER) + .fillColor(TINT) + .center(glyph.build()) + .build(); + } + + /** The degree, the institution and the place-and-years line, as one column. */ + private static DocumentNode educationLines(CvEntry entry) { + DocumentTextStyle place = style(BODY_FONT, EDU_DETAIL_SIZE, MUTED, false); + DocumentTextStyle years = style(BODY_FONT, EDU_DETAIL_SIZE, ACCENT, false); + double width = EDUCATION_COLUMN - BADGE_COLUMN - TABLE_SLACK; + ParagraphBuilder degree = new ParagraphBuilder() + .name("Degree") + .text(entry.title()) + .lineSpacing(0) + .textStyle(style(BODY_FONT, DEGREE_SIZE, INK, true)); + if (!entry.link().isBlank()) { + degree.link(new DocumentLinkOptions(entry.link())); + } + ParagraphBuilder detail = new ParagraphBuilder() + .name("EducationDetail") + .lineSpacing(0) + .textStyle(place) + .inlineText(entry.place(), place); + if (!entry.date().isBlank()) { + detail.inlineText(PIPE, place).inlineText(entry.date(), years); + } + return new TableBuilder() + .name("EducationLines") + .margin(new DocumentInsets(EDUCATION_TEXT_TOP, 0, 0, BADGE_COLUMN)) + .width(width) + .columns(DocumentTableColumn.fixed(width)) + .defaultCellStyle(cellStyle(DocumentInsets.zero(), + DocumentTableTextAnchor.TOP_LEFT)) + .rowCells(DocumentTableCell.node(degree.build()) + .withStyle(cellStyle( + new DocumentInsets(0, 0, DEGREE_TO_INSTITUTION, 0), + DocumentTableTextAnchor.TOP_LEFT))) + .rowCells(DocumentTableCell.node(text("Institution", entry.subtitle(), + style(BODY_FONT, INSTITUTION_SIZE, BODY, false))) + .withStyle(cellStyle( + new DocumentInsets(0, 0, INSTITUTION_TO_DETAIL, 0), + DocumentTableTextAnchor.TOP_LEFT))) + .rowCells(DocumentTableCell.node(detail.build())) + .build(); + } + + /** + * The languages — a name, the level as the document words it, and a + * five-disc rating per row. + * + *

The rating paragraph is styled although it carries no words: a + * paragraph takes its line box from its type whether or not a glyph uses + * it, and the default box would set the whole row's pitch.

+ */ + private static void renderLanguages(SectionBuilder block, SkillsSection languages) { + block.spacing(0); + if (languages == null) { + return; + } + List entries = new ArrayList<>(); + for (SkillGroup group : languages.groups()) { + entries.addAll(group.entries()); + } + if (entries.isEmpty()) { + return; + } + block.addTable(table -> { + table.name("LanguageTable"); + table.margin(new DocumentInsets(LANGUAGES_BODY_OFFSET, 0, 0, 0)); + table.width(LANGUAGES_COLUMN - TABLE_SLACK); + table.columns( + DocumentTableColumn.fixed(LANGUAGE_NAME_COLUMN), + DocumentTableColumn.fixed(LANGUAGE_LEVEL_COLUMN), + DocumentTableColumn.fixed(LANGUAGE_RATING_COLUMN - TABLE_SLACK)); + table.defaultCellStyle(cellStyle( + new DocumentInsets(0, 0, + gap(LANGUAGE_PITCH, LANGUAGE_SIZE * LINE_FACTOR), 0), + DocumentTableTextAnchor.CENTER_LEFT)); + for (int index = 0; index < entries.size(); index++) { + CvSkill skill = entries.get(index); + table.rowCells( + DocumentTableCell.node(text("LanguageName_" + index, skill.name(), + style(BODY_FONT, LANGUAGE_SIZE, BODY, false))), + DocumentTableCell.node(text("LanguageLevel_" + index, skill.note(), + style(BODY_FONT, LEVEL_SIZE, MUTED, false))), + DocumentTableCell.node(rating("LanguageRating_" + index, skill))); + } + table.rowStyle(entries.size() - 1, + cellStyle(DocumentInsets.zero(), DocumentTableTextAnchor.CENTER_LEFT)); + }); + } + + /** One language's discs, filled up to its rating and grey after it. */ + private static DocumentNode rating(String name, CvSkill skill) { + DocumentTextStyle box = style(BODY_FONT, LANGUAGE_SIZE, BODY, false); + ParagraphBuilder paragraph = new ParagraphBuilder(); + paragraph.name(name); + paragraph.lineSpacing(0); + paragraph.textStyle(box); + if (skill.level().isEmpty()) { + return paragraph.build(); + } + int filled = (int) Math.round(skill.level().getAsDouble() * RATING_SCALE); + for (int index = 0; index < RATING_SCALE; index++) { + paragraph.dot(RATING_DOT_DIAMETER, index < filled ? ACCENT : RATING_EMPTY); + if (index < RATING_SCALE - 1) { + paragraph.inlineText(RATING_GAP, box); + } + } + return paragraph.build(); + } + + // -- the closing band -------------------------------------------------- + + /** + * The closing band. + * + *

A fill with a corner radius and its own padding rather than a soft + * panel, whose padding is a single uniform value: this band's side padding + * is several times its vertical padding.

+ * + *

The quotation mark is a packaged glyph rather than a typographic one. + * At the size the design draws it a real quotation mark would need about + * forty points of type, and its line box alone would be nearly twice the + * band's height.

+ */ + static void renderQuote(PageFlowBuilder page, ParagraphSection quote) { + if (quote == null || quote.body().isBlank()) { + return; + } + DocumentTextStyle line = style(BODY_FONT, QUOTE_SIZE, BODY, false); + DocumentTextStyle box = style(BODY_FONT, QUOTE_LINE_BOX / LINE_FACTOR, BODY, false); + page.addSection("QuoteBand", band -> { + band.spacing(0); + band.margin((float) CREDENTIALS_TO_QUOTE, 0f, 0f, 0f); + band.fillColor(TINT); + band.cornerRadius(QUOTE_RADIUS); + band.padding((float) QUOTE_PAD_V, (float) QUOTE_PAD_H, + (float) QUOTE_PAD_V, (float) QUOTE_PAD_H); + band.addParagraph(p -> { + p.name("QuoteLine"); + p.lineSpacing(0); + p.textStyle(box); + inlineIcon(p, VioletGridIcons.QUOTE); + p.inlineText(QUOTE_GAP, line); + p.inlineText(quote.body().replace(String.valueOf((char) 10), " "), line); + }); + }); + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridIcons.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridIcons.java new file mode 100644 index 000000000..823b328c9 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridIcons.java @@ -0,0 +1,112 @@ +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.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Classpath loader for the Violet Grid CV marks: the five contact glyphs, the + * six skill marks, the two project glyphs, the graduation cap and the + * quotation mark that opens the closing band. + * + *

They ship inside the templates artifact under + * {@code templates/cv/violet-grid/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.

+ * + *

{@link #ENTRY_TOKENS} is the vocabulary a document names through + * {@code CvEntry.icon()}. It is scoped to this preset — another preset + * packages its own set — and each mark carries the width it is drawn at, + * because an icon's box is not its glyph and the sizes are per-kind.

+ */ +final class VioletGridIcons { + + static final String EMAIL = "email"; + static final String PHONE = "phone"; + static final String LOCATION = "location"; + static final String WEBSITE = "website"; + static final String LINKEDIN = "linkedin"; + static final String GRADUATION = "graduation"; + static final String QUOTE = "quote"; + + private static final String ICON_ROOT = "/templates/cv/violet-grid/icons/"; + + /** The width each mark is drawn at, as the asset resolver chose it. */ + private static final Map SIZES = Map.ofEntries( + Map.entry(EMAIL, 11.5), + Map.entry(PHONE, 11.5), + Map.entry(LOCATION, 11.5), + Map.entry(WEBSITE, 11.5), + Map.entry(LINKEDIN, 11.5), + Map.entry("ux-research", 20.5), + Map.entry("information-architecture", 20.5), + Map.entry("wireframing", 20.5), + Map.entry("prototyping", 20.5), + Map.entry("usability-testing", 20.5), + Map.entry("design-systems", 20.5), + Map.entry("project-wallet", 28.0), + Map.entry("project-health", 28.0), + Map.entry(GRADUATION, 26.0), + Map.entry(QUOTE, 14.1)); + + /** The marks a document may name on a skill, a project or a degree. */ + static final Set ENTRY_TOKENS = Set.of( + "ux-research", "information-architecture", "wireframing", "prototyping", + "usability-testing", "design-systems", "project-wallet", "project-health", + GRADUATION); + + private static final Map CACHE = new ConcurrentHashMap<>(); + + private VioletGridIcons() { + } + + /** + * The width one mark is drawn at. + * + * @param token one of the packaged tokens + * @return the drawn width in points + * @throws IllegalArgumentException when the token names no packaged mark, + * listing the ones a document may choose — an icon token is data, + * so a wrong one is a data error + */ + static double size(String token) { + Double size = SIZES.get(token); + if (size == null) { + throw new IllegalArgumentException( + "Unknown violet grid CV icon token '" + token + + "'. This preset packages " + ENTRY_TOKENS.stream().sorted().toList() + + " for an entry mark."); + } + return size; + } + + /** + * Reads one mark. + * + * @param token one of the packaged tokens + * @return the parsed icon + */ + static SvgIcon icon(String token) { + size(token); + return CACHE.computeIfAbsent(token, VioletGridIcons::read); + } + + private static SvgIcon read(String token) { + String resourcePath = ICON_ROOT + token + ".svg"; + try (InputStream input = VioletGridIcons.class.getResourceAsStream(resourcePath)) { + if (input == null) { + throw new IllegalStateException("Missing violet grid CV icon: " + resourcePath); + } + return SvgIcon.parse(new String(input.readAllBytes(), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException( + "Failed to read violet grid CV icon: " + resourcePath, e); + } + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridMasthead.java new file mode 100644 index 000000000..7ae5ce376 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridMasthead.java @@ -0,0 +1,214 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.node.DocumentLinkOptions; +import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.style.DocumentTextStyle; +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 com.demcha.compose.document.templates.cv.data.ParagraphSection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ACCENT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ACCENT_LIGHT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BODY; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BODY_FONT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.CONTACT_COLUMN_X; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.CONTACT_ICON_GAP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.CONTACT_PITCH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.CONTACT_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.CONTACT_TOP; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DISCIPLINE_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DISCIPLINE_TO_RULE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DISCIPLINE_TRACKING; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DISPLAY_FONT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADER_TO_SUMMARY; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.IDENTITY_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.IDENTITY_RULE_WIDTH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.INK; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.LINE_FACTOR; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.NAME_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SUMMARY_PITCH; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SUMMARY_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.gap; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.style; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.inlineIcon; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.lines; +import static com.demcha.compose.document.templates.cv.presets.VioletGridWidgets.tracked; + +/** + * The top of the sheet: the two-tone name beside the contact column, and the + * opening lines under them. + */ +final class VioletGridMasthead { + + private VioletGridMasthead() { + } + + /** One line of the contact column: a mark, a value and where it points. */ + private record Channel(String token, String value, String href) { + } + + /** + * The masthead, as a top-level row split where the contact marks begin — + * the column is left-aligned on its marks rather than right-aligned to the + * margin, so that x is the split. + */ + static void render(PageFlowBuilder page, CvIdentity identity, ParagraphSection summary) { + page.addRow("Header", row -> { + row.spacing(0); + row.columns( + DocumentRowColumn.fixed(CONTACT_COLUMN_X), + DocumentRowColumn.weight(1.0)); + row.addSection("Identity", block -> renderIdentity(block, identity)); + row.addSection("Contact", block -> renderContact(block, identity)); + }); + renderSummary(page, summary); + } + + // -- the name ---------------------------------------------------------- + + /** + * The name, the discipline line and the accent rule under it. + * + *

Both names are inline runs of ONE paragraph, so the gap between them + * is a real word space that follows the type size rather than a measured + * offset that would not.

+ */ + private static void renderIdentity(SectionBuilder block, CvIdentity identity) { + block.spacing(0); + DocumentTextStyle given = style(DISPLAY_FONT, NAME_SIZE, INK, false); + DocumentTextStyle family = style(DISPLAY_FONT, NAME_SIZE, ACCENT_LIGHT, false); + block.addParagraph(p -> p + .name("Name") + .lineSpacing(0) + .textStyle(given) + .inlineText(identity.name().first().toUpperCase(Locale.ROOT), given) + .inlineText(" ", given) + .inlineText(identity.name().last().toUpperCase(Locale.ROOT), family)); + block.add(tracked("Discipline", identity.jobTitle().toUpperCase(Locale.ROOT), + DISPLAY_FONT, DISCIPLINE_SIZE, ACCENT, true, DISCIPLINE_TRACKING) + .margin(0f, 0f, (float) DISCIPLINE_TO_RULE, 0f) + .build()); + block.addLine(line -> line + .name("IdentityRule") + .horizontal(IDENTITY_RULE_WIDTH) + .thickness(IDENTITY_RULE_THICKNESS) + .color(ACCENT)); + } + + // -- the contact column ------------------------------------------------ + + /** + * The contact lines. + * + *

Each is one paragraph rather than a two-column table: the mark is an + * inline run measured into the line box, so it shares a baseline with its + * value by construction, and nothing in this column has to line up with + * anything outside it. Where the value points somewhere the run becomes an + * inline link, so the target reaches the PDF as a real annotation.

+ * + *

A link shows its own label and carries the address behind it, so the + * column's width does not depend on how long an address happens to be.

+ */ + private static void renderContact(SectionBuilder block, CvIdentity identity) { + block.spacing(0); + block.padding((float) CONTACT_TOP, 0f, 0f, 0f); + DocumentTextStyle value = style(BODY_FONT, CONTACT_SIZE, BODY, false); + // The line box comes from the paragraph's own style, and an inline run + // taller than it is drawn clipped: take whichever of the mark and the + // text needs more room. + double boxSize = Math.max(CONTACT_SIZE, + VioletGridIcons.size(VioletGridIcons.EMAIL) / LINE_FACTOR); + DocumentTextStyle box = style(BODY_FONT, boxSize, BODY, false); + List channels = channels(identity); + for (int i = 0; i < channels.size(); i++) { + Channel channel = channels.get(i); + boolean last = i == channels.size() - 1; + int index = i; + block.addParagraph(p -> { + p.name("Contact_" + index + "_" + channel.token()); + p.lineSpacing(0); + p.textStyle(box); + inlineIcon(p, channel.token()); + p.inlineText(CONTACT_ICON_GAP, value); + if (channel.href() == null || channel.href().isBlank()) { + p.inlineText(channel.value(), value); + } else { + p.inlineText(channel.value(), value, new DocumentLinkOptions(channel.href())); + } + p.margin(0f, 0f, + last ? 0f : (float) gap(CONTACT_PITCH, boxSize * LINE_FACTOR), 0f); + }); + } + } + + /** The column's order: how to write, how to call, where, then the links. */ + private static List channels(CvIdentity identity) { + Contact contact = identity.contact(); + List channels = new ArrayList<>(); + channels.add(new Channel(VioletGridIcons.EMAIL, contact.email(), + "mailto:" + contact.email())); + channels.add(new Channel(VioletGridIcons.PHONE, contact.phone(), + telUri(contact.phone()))); + channels.add(new Channel(VioletGridIcons.LOCATION, contact.address(), null)); + for (Link link : identity.links()) { + channels.add(new Channel(markFor(link), link.label(), link.url())); + } + return channels; + } + + /** A link takes the mark of the network it points at, or the globe. */ + private static String markFor(Link link) { + String target = (link.url() + " " + link.label()).toLowerCase(Locale.ROOT); + return target.contains("linkedin") ? VioletGridIcons.LINKEDIN : VioletGridIcons.WEBSITE; + } + + /** + * 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; + } + + // -- the opening lines ------------------------------------------------- + + /** + * The opening block — one paragraph per line the document wrote. + * + *

Not a wrapped paragraph, and that is a reading of the design rather + * than a convenience: its first line ends with room for the next word + * several times over, so no greedy wrap could produce it. The lines are + * sentences, set as lines, and the document says so.

+ */ + private static void renderSummary(PageFlowBuilder page, ParagraphSection summary) { + if (summary == null || summary.body().isBlank()) { + return; + } + DocumentTextStyle line = style(BODY_FONT, SUMMARY_SIZE, BODY, false); + List body = lines(summary.body()); + for (int i = 0; i < body.size(); i++) { + boolean last = i == body.size() - 1; + int index = i; + page.addParagraph(p -> p + .name("SummaryLine_" + index) + .text(body.get(index)) + .lineSpacing(0) + .textStyle(line) + .margin(index == 0 ? (float) HEADER_TO_SUMMARY : 0f, 0f, + last ? 0f : (float) gap(SUMMARY_PITCH, SUMMARY_SIZE * LINE_FACTOR), + 0f)); + } + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridStyles.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridStyles.java new file mode 100644 index 000000000..81554035e --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridStyles.java @@ -0,0 +1,421 @@ +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.DocumentInsets; +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.table.DocumentTableStyle; +import com.demcha.compose.document.table.DocumentTableTextAnchor; +import com.demcha.compose.font.FontName; + +/** + * The measured geometry, palette and type scale of the Violet Grid CV. + * + *

Every length is a measurement, converted once

+ * + *

The design was drawn on a 1055-pixel-wide raster, so each length here is + * written as {@link #px} of a pixel measured off it. That keeps the source + * auditable — a reader can put a ruler on the drawing and find the number — + * and keeps one conversion factor rather than a page of pre-multiplied + * decimals. Widths that are shares of the page derive from + * {@link #CONTENT_WIDTH}; only genuinely independent marks (a disc's + * diameter, a rule's thickness) are their own constants.

+ * + *

The type sizes are solved, not estimated

+ * + *

Each is the drawing's measured ink width for one specific string divided + * by that string's width in ems in the face that sets it. An estimate from an + * average advance came out several per cent small everywhere, which is enough + * to move a line break — and a moved break is the difference a pixel gate can + * see.

+ */ +final class VioletGridStyles { + + private VioletGridStyles() { + } + + // -- the grid ---------------------------------------------------------- + + /** The drawing's raster width against A4's, in points per pixel. */ + static final double PX_TO_PT = 595.276 / 1055.0; + + static final DocumentPageSize PAGE = DocumentPageSize.A4; + + /** + * One measurement off the drawing, in points. + * + * @param pixels the pixel measured on the 1055-wide raster + * @return the same distance on the page + */ + static double px(double pixels) { + return pixels * PX_TO_PT; + } + + /** The engine's line box as a multiple of the type size. */ + static final double LINE_FACTOR = 1.2; + + /** + * A hundredth of a point of slack under every table. The engine compares a + * table's declared width against its container's to the last bit, and a + * sum of derived doubles lands a ten-thousandth of a point over — enough + * to be refused. + */ + static final double TABLE_SLACK = 0.01; + + /** + * No cell borders. A table draws its rules per cell from that cell's own + * style, and the default style has one; every table here is a layout + * mechanism rather than a grid the reader should see, so each states the + * absence explicitly. + */ + static final DocumentStroke NO_BORDER = + DocumentStroke.of(DocumentColor.rgba(0, 0, 0, 0), 0.0); + + // -- palette ----------------------------------------------------------- + + /** The violet everything but the family name is set in. */ + static final DocumentColor ACCENT = DocumentColor.rgb(118, 78, 175); + + /** + * The lighter violet the family name takes. Two tones, not one: the + * drawing sets them measurably apart, and antialiasing only lightens — so + * the difference is a decision rather than noise. + */ + static final DocumentColor ACCENT_LIGHT = DocumentColor.rgb(141, 105, 193); + + /** The hairline that follows every section heading — the accent at half. */ + static final DocumentColor ACCENT_RULE = DocumentColor.rgb(190, 172, 220); + + /** The tint behind the project tiles, the education badge and the band. */ + static final DocumentColor TINT = DocumentColor.rgb(237, 231, 245); + + static final DocumentColor INK = DocumentColor.rgb(28, 26, 32); + static final DocumentColor BODY = DocumentColor.rgb(58, 56, 72); + static final DocumentColor MUTED = DocumentColor.rgb(96, 94, 110); + static final DocumentColor RULE = DocumentColor.rgb(219, 219, 219); + static final DocumentColor RATING_EMPTY = DocumentColor.rgb(226, 226, 226); + + // -- type -------------------------------------------------------------- + + /** + * The display face: the name, the discipline line, the section headings + * and the skill labels. Chosen by measurement: the drawing sets the name + * at a width-to-cap ratio this face matches within a few per cent, where + * the nearest-looking alternative is a quarter too wide. + */ + static final FontName DISPLAY_FONT = FontName.ASAP_CONDENSED; + + /** The body face: everything the display face does not set. */ + static final FontName BODY_FONT = FontName.LATO; + + static final double NAME_SIZE = 46.55; + static final double DISCIPLINE_SIZE = 17.73; + static final double HEADING_SIZE = 12.90; + + /** + * The skill labels, sized so the longest one clears its column's gutter. + * + *

Solved from the drawing's own ink widths the size comes out larger — + * but at that size the longest label is wider than its column less the + * gutter, and the binding constraint is the gutter rather than the ink. All + * six are set at one size even though only one is constrained: six labels + * of a set at five sizes is a worse document than six one step small.

+ */ + static final double LABEL_SIZE = 6.8; + + static final double CONTACT_SIZE = 8.93; + static final double SUMMARY_SIZE = 9.31; + static final double SKILL_DESC_SIZE = 6.35; + static final double TOOL_SIZE = 8.11; + static final double PERIOD_SIZE = 8.67; + static final double ROLE_SIZE = 9.42; + static final double EMPLOYER_SIZE = 8.09; + static final double LOCATION_SIZE = 8.76; + static final double HIGHLIGHT_SIZE = 8.02; + static final double PROJECT_TITLE_SIZE = 9.62; + static final double PROJECT_SUB_SIZE = 8.35; + static final double PROJECT_YEAR_SIZE = 8.2; + static final double PROJECT_BODY_SIZE = 8.47; + static final double DEGREE_SIZE = 8.93; + static final double INSTITUTION_SIZE = 8.96; + static final double EDU_DETAIL_SIZE = 8.55; + static final double LANGUAGE_SIZE = 8.49; + static final double LEVEL_SIZE = 7.85; + static final double QUOTE_SIZE = 8.64; + + // -- tracking ---------------------------------------------------------- + + /** + * A space's advance in the body face, as a fraction of the em. + * + *

Deliberately the BODY face's number even though tracking is only ever + * applied to display text: the display face's space is narrow enough that + * a gap of the wanted width would need a space run taller than the type it + * separates, and an inline run sets the line box. The body face's space is + * nearly twice as wide, so the same gap keeps the line box the type's — and + * a space has no glyph to give the substitution away.

+ */ + static final double SPACE_RATIO = 0.256; + + /** + * Tracking in points between one letter and the next. The headings' figure + * is an average over all six: solved one at a time they range too widely + * for any single typeface setting, because the drawing is a raster whose + * glyph advances are not internally consistent — six per-heading constants + * would be fitting noise rather than reproducing a design. + */ + static final double DISCIPLINE_TRACKING = 3.15; + static final double HEADING_TRACKING = 0.77; + + /** Gaps measured as constants whatever the words are, written as spaces. */ + static final String CONTACT_ICON_GAP = " "; + static final String TOOL_GAP = " "; + static final String PIPE = " | "; + static final String RATING_GAP = " "; + static final String QUOTE_GAP = " "; + + // -- the page ---------------------------------------------------------- + + static final double PAGE_MARGIN_X = px(53); + + /** + * Back-calculated rather than measured: the first ink on the page is a cap, + * and a line box starts above its caps, so the margin is where the name's + * box has to begin for its caps to land where the drawing puts them. + */ + static final double PAGE_MARGIN_TOP = px(24); + + /** + * Measured under the quote band, then taken in by three pixels. Nothing on + * this page sits at the bottom margin except the closing band, and the + * drawing puts its lower edge exactly ON the margin — so at the measured + * value the flow has no slack and the band's last points spill onto a + * second page. Three pixels of headroom is the difference between a + * one-page CV and a two-page one. + */ + static final double PAGE_MARGIN_BOTTOM = px(18); + + static final double CONTENT_WIDTH = 595.276 - 2.0 * PAGE_MARGIN_X; + + // -- the masthead ------------------------------------------------------ + + /** Where the contact marks begin, which is where the masthead row splits. */ + static final double CONTACT_COLUMN_X = px(653); + static final double CONTACT_TOP = px(8); + static final double CONTACT_PITCH = px(34.3); + static final double DISCIPLINE_TO_RULE = px(23.3); + static final double IDENTITY_RULE_WIDTH = px(70); + static final double IDENTITY_RULE_THICKNESS = px(4); + + /** + * Measured ink to ink, then corrected by two pixels against the render: + * the measurement is a cap-to-cap distance and what the engine wants is a + * margin under a line BOX, which grew when the name's size was solved. + */ + static final double HEADER_TO_SUMMARY = px(26); + static final double SUMMARY_PITCH = px(22); + + // -- headings ---------------------------------------------------------- + + static final double HEADING_TO_RULE = px(20); + static final double HEADING_RULE_THICKNESS = px(1.5); + + /** + * How far below the heading's line top the rule sits. The drawing centres + * it on the cap band rather than on the line box, and the two differ by + * the leading above the caps. + */ + static final double HEADING_RULE_OFFSET = px(13.5); + + // -- the skills grid --------------------------------------------------- + + static final double SUMMARY_TO_SKILLS = px(33.8); + static final double HEADING_TO_SKILLS = px(19.5); + static final double ICON_TO_LABEL = px(17.4); + static final double LABEL_TO_DESC = px(6); + static final double SKILL_DESC_PITCH = px(18); + static final double SKILL_DIVIDER_THICKNESS = px(2); + + /** + * The gutter between a column's content and the separators either side. + * + *

The drawing gives its widest column more room than six equal columns + * leave, so the label that just fits there is flush here. This is that + * column's own narrowest gutter, applied to every column so the grid keeps + * one rhythm.

+ */ + static final double SKILL_COLUMN_INSET = px(11); + + /** + * The measure the descriptions wrap to, which is narrower than the column. + * + *

Solved from the drawing's own line breaks rather than measured off + * it: greedy-wrapping all six descriptions, each column's breaks are + * reproduced by an interval of measures, and this is the value inside the + * most of them. No single measure reproduces all six — the drawing's own + * metrics disagree with any one face — so five of six is the best + * available.

+ */ + static final double SKILL_DESC_MEASURE = 72.5; + + /** + * The dotted separator's height — the one length on this page that is not + * derived from what it sits beside, and a known limit: a skill whose + * description ran to four lines would outgrow its separator. The + * alternative primitive stretches to the content but cannot be dashed, and + * the dotted rule is a visible feature of the design. + */ + static final double SKILL_RAIL_HEIGHT = px(136); + static final double SKILL_DASH_ON = px(2); + static final double SKILL_DASH_OFF = px(3.15); + + // -- the tools strip --------------------------------------------------- + + static final double SKILLS_TO_TOOLS = px(27.3); + static final double HEADING_TO_TOOLS = px(18.7); + static final double TOOL_DOT_DIAMETER = px(5.2); + + // -- the timeline ------------------------------------------------------ + + static final double TOOLS_TO_EXPERIENCE = px(35.7); + static final double HEADING_TO_ENTRIES = px(19.3); + + /** Where the rail stands, as an indent from the page margin. */ + static final double RAIL_INDENT = px(134.5); + + /** The gap between the rail and an entry's own content. */ + static final double ENTRY_INDENT = px(35.5); + static final double RAIL_THICKNESS = px(2); + static final double MARKER_DIAMETER = px(13.5); + static final double ENTRY_WIDTH = + CONTENT_WIDTH - RAIL_INDENT - ENTRY_INDENT - TABLE_SLACK; + static final double LOCATION_COLUMN = px(200); + static final double ROLE_COLUMN = ENTRY_WIDTH - LOCATION_COLUMN; + + /** The dates sit back at the page margin, outside the rail. */ + static final double DATE_OFFSET = -(RAIL_INDENT + ENTRY_INDENT); + + /** Half the disc, plus the indent, puts its centre on the border. */ + static final double MARKER_OFFSET = -(ENTRY_INDENT + MARKER_DIAMETER / 2.0); + + static final double BULLET_COLUMN = px(16); + static final double BULLET_DOT_DIAMETER = px(6); + static final double BULLET_PITCH = px(20.5); + static final double TITLE_TO_BULLETS = px(6.1); + static final double ENTRY_GAP = px(24); + + // -- the projects ------------------------------------------------------ + + static final double EXPERIENCE_TO_PROJECTS = px(27.6); + static final double HEADING_TO_PROJECTS = px(15.1); + static final double PROJECT_GAP = px(17.4); + static final double THUMB_COLUMN = px(138.5); + static final double THUMB_PAD_LEFT = px(21); + static final double THUMB_WIDTH = px(80); + static final double THUMB_HEIGHT = px(76); + static final double THUMB_RADIUS = px(10); + static final double PROJECT_COPY_INDENT = px(34.5); + static final double PROJECT_RULE_THICKNESS = px(2); + static final double PROJECT_COPY_WIDTH = + CONTENT_WIDTH - THUMB_COLUMN - PROJECT_COPY_INDENT - TABLE_SLACK; + static final double PROJECT_YEAR_COLUMN = px(120); + static final double PROJECT_TITLE_COLUMN = PROJECT_COPY_WIDTH - PROJECT_YEAR_COLUMN; + static final double TITLE_TO_BODY = px(3.6); + static final double PROJECT_BODY_PITCH = px(19.5); + + // -- the credentials band ---------------------------------------------- + + static final double PROJECTS_TO_CREDENTIALS = px(15.7); + static final double EDUCATION_COLUMN = px(430); + static final double CREDENTIAL_GUTTER = px(70); + static final double LANGUAGES_COLUMN = + CONTENT_WIDTH - EDUCATION_COLUMN - CREDENTIAL_GUTTER - TABLE_SLACK; + + /** How the two heading rules split what the two heading texts leave. */ + static final double EDUCATION_RULE_WEIGHT = 335.0 / 680.0; + static final double LANGUAGES_RULE_WEIGHT = 345.0 / 680.0; + + static final double HEADING_TO_EDUCATION = px(13.5); + static final double BADGE_DIAMETER = px(72); + static final double BADGE_COLUMN = px(91); + static final double EDUCATION_TEXT_TOP = px(4.1); + static final double DEGREE_TO_INSTITUTION = px(3); + static final double INSTITUTION_TO_DETAIL = px(8.7); + + /** + * The languages rows start a shade below where the education badge does. + * The bodies row already carries the heading gap; this is only the + * difference between the two halves. + */ + static final double LANGUAGES_BODY_OFFSET = px(15.8 - 13.5); + + static final double LANGUAGE_NAME_COLUMN = px(162); + static final double LANGUAGE_LEVEL_COLUMN = px(194); + static final double LANGUAGE_RATING_COLUMN = + LANGUAGES_COLUMN - LANGUAGE_NAME_COLUMN - LANGUAGE_LEVEL_COLUMN; + static final double LANGUAGE_PITCH = px(27.5); + static final double RATING_DOT_DIAMETER = px(10); + + /** How many discs a rating is drawn out of. */ + static final int RATING_SCALE = 5; + + // -- the closing band -------------------------------------------------- + + static final double CREDENTIALS_TO_QUOTE = px(24); + static final double QUOTE_RADIUS = px(6); + static final double QUOTE_PAD_H = px(40); + + /** + * The line box the band has to hold. The quotation mark is taller than the + * type beside it, and an inline run taller than its paragraph's line box is + * drawn clipped — so the box is the mark's height. + */ + static final double QUOTE_LINE_BOX = + Math.max(QUOTE_SIZE * LINE_FACTOR, VioletGridIcons.size(VioletGridIcons.QUOTE)); + + /** The band's measured height less what it holds, halved. */ + static final double QUOTE_PAD_V = (px(48) - QUOTE_LINE_BOX) / 2.0; + + // -- helpers ----------------------------------------------------------- + + static DocumentTextStyle style(FontName font, double size, DocumentColor color, + boolean bold) { + return DocumentTextStyle.builder() + .fontName(font) + .size(size) + .color(color) + .decoration(bold ? DocumentTextDecoration.BOLD : DocumentTextDecoration.DEFAULT) + .build(); + } + + /** A cell's padding and how its content sits in it. */ + static DocumentTableStyle cellStyle(DocumentInsets padding, + DocumentTableTextAnchor anchor) { + return DocumentTableStyle.builder() + .padding(padding) + .textAnchor(anchor) + .stroke(NO_BORDER) + .build(); + } + + /** A measured pitch, minus the content it has to clear, is the gap that produces it. */ + static double gap(double pitch, double contentHeight) { + return Math.max(0.0, pitch - contentHeight); + } + + /** + * A measured pitch as the additive value {@code lineSpacing} actually is: + * the line box is {@link #LINE_FACTOR} times the type size and the setting + * adds points on top of it. + */ + static double leading(double pitch, double size) { + return Math.max(0.0, pitch - LINE_FACTOR * size); + } + + /** How far to drop a mark of this height to centre it on a title's line box. */ + static double centred(double markHeight) { + return Math.max(0.0, (ROLE_SIZE * LINE_FACTOR - markHeight) / 2.0); + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridWidgets.java new file mode 100644 index 000000000..6b215eeab --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridWidgets.java @@ -0,0 +1,147 @@ +package com.demcha.compose.document.templates.cv.presets; + +import com.demcha.compose.document.dsl.ParagraphBuilder; +import com.demcha.compose.document.dsl.RowBuilder; +import com.demcha.compose.document.node.DocumentNode; +import com.demcha.compose.document.node.InlineImageAlignment; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.font.FontName; + +import java.util.ArrayList; +import java.util.List; + +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ACCENT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.ACCENT_RULE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.BODY_FONT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.DISPLAY_FONT; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_RULE_OFFSET; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_RULE_THICKNESS; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_SIZE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TRACKING; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TO_RULE; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SPACE_RATIO; +import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.style; + +/** + * The idioms this sheet repeats: the letter-spaced run, the heading followed + * by its hairline, a disc on its own, and a line of text as a node. + */ +final class VioletGridWidgets { + + private VioletGridWidgets() { + } + + /** + * Letter-spaced text, as one inline run per character. + * + *

A text style carries no tracking, so the gap between two letters is a + * space run set at whatever size makes that space the wanted number of + * points wide — which is why the tracking is a number rather than a fixed + * word space. The gap is set in the BODY face even under display text: the + * display face's space is narrow enough that the wanted gap would need a + * run taller than the type it separates, and an inline run sets the line + * box. A space has no glyph to give the substitution away.

+ * + * @param name the node name + * @param text the text to space out + * @param font the face the letters are set in + * @param size the size the letters are set at + * @param color the colour of the letters + * @param bold whether the letters are bold + * @param tracking the gap in points; zero or less writes the text plainly + * @return the paragraph, unbuilt, so a caller can still give it a margin + */ + static ParagraphBuilder tracked(String name, String text, FontName font, double size, + DocumentColor color, boolean bold, double tracking) { + DocumentTextStyle glyph = style(font, size, color, bold); + ParagraphBuilder paragraph = new ParagraphBuilder(); + paragraph.name(name); + paragraph.lineSpacing(0); + paragraph.textStyle(glyph); + if (tracking <= 0.0) { + paragraph.text(text); + return paragraph; + } + DocumentTextStyle gap = style(BODY_FONT, tracking / SPACE_RATIO, color, bold); + for (int index = 0; index < text.length(); index++) { + paragraph.inlineText(String.valueOf(text.charAt(index)), glyph); + if (index < text.length() - 1) { + paragraph.inlineText(" ", gap); + } + } + return paragraph; + } + + /** + * A tracked caps heading followed by a hairline to the right margin. + * + *

The text column takes what its words need and the rule column takes + * what is left, which is what makes the rule start where the text ends + * without anything having to know how wide the text is. Returned as a node + * rather than added to a builder, so one method serves callers holding + * different builder types.

+ */ + static DocumentNode headingRow(String name, String text, double topMargin) { + RowBuilder row = new RowBuilder(); + row.name(name + "Heading"); + row.spacing(0); + row.margin(new DocumentInsets(topMargin, 0, 0, 0)); + row.columns(DocumentRowColumn.auto(), DocumentRowColumn.weight(1.0)); + row.addSection(name + "HeadingText", cell -> { + cell.spacing(0); + cell.add(tracked(name + "HeadingLabel", text, DISPLAY_FONT, HEADING_SIZE, + ACCENT, true, HEADING_TRACKING).build()); + }); + row.addSection(name + "HeadingRule", cell -> { + cell.spacing(0); + cell.padding((float) HEADING_RULE_OFFSET, 0f, 0f, (float) HEADING_TO_RULE); + cell.addLine(line -> line + .name(name + "HeadingRuleLine") + .fill() + .thickness(HEADING_RULE_THICKNESS) + .color(ACCENT_RULE)); + }); + return row.build(); + } + + /** A disc on its own, in a paragraph whose line box is set by its type. */ + static DocumentNode markerDot(String name, double diameter, double boxSize, + DocumentColor fill) { + ParagraphBuilder paragraph = new ParagraphBuilder(); + paragraph.name(name); + paragraph.lineSpacing(0); + paragraph.textStyle(style(BODY_FONT, boxSize, fill, false)); + paragraph.dot(diameter, fill); + return paragraph.build(); + } + + /** One line of text as a node. */ + static DocumentNode text(String name, String value, DocumentTextStyle textStyle) { + return new ParagraphBuilder() + .name(name) + .text(value) + .lineSpacing(0) + .textStyle(textStyle) + .build(); + } + + /** Places a mark inline, on the baseline of the paragraph it opens. */ + static void inlineIcon(ParagraphBuilder paragraph, String token) { + paragraph.inlineSvgIcon(VioletGridIcons.icon(token), VioletGridIcons.size(token), + InlineImageAlignment.CENTER); + } + + /** 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/resources/templates/cv/violet-grid/icons/design-systems.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/design-systems.svg new file mode 100644 index 000000000..abccd250f --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/design-systems.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/email.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/email.svg new file mode 100644 index 000000000..24628a6eb --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/graduation.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/graduation.svg new file mode 100644 index 000000000..f89ae1c16 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/graduation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/information-architecture.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/information-architecture.svg new file mode 100644 index 000000000..ebadcc790 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/information-architecture.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/linkedin.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/linkedin.svg new file mode 100644 index 000000000..ab2d1bd8e --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/linkedin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/location.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/location.svg new file mode 100644 index 000000000..fde000d03 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/location.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/phone.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/phone.svg new file mode 100644 index 000000000..7c823dae3 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/project-health.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/project-health.svg new file mode 100644 index 000000000..d42590cf3 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/project-health.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/project-wallet.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/project-wallet.svg new file mode 100644 index 000000000..70bd4b1c6 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/project-wallet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/prototyping.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/prototyping.svg new file mode 100644 index 000000000..6dc414529 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/prototyping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/quote.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/quote.svg new file mode 100644 index 000000000..281a38b10 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/usability-testing.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/usability-testing.svg new file mode 100644 index 000000000..b2857b690 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/usability-testing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/ux-research.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/ux-research.svg new file mode 100644 index 000000000..31f42319d --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/ux-research.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/website.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/website.svg new file mode 100644 index 000000000..faddf384b --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/website.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/main/resources/templates/cv/violet-grid/icons/wireframing.svg b/templates/src/main/resources/templates/cv/violet-grid/icons/wireframing.svg new file mode 100644 index 000000000..0273be191 --- /dev/null +++ b/templates/src/main/resources/templates/cv/violet-grid/icons/wireframing.svg @@ -0,0 +1 @@ + \ No newline at end of file