[
  {
    "path": ".eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  env: { browser: true, es2020: true },\n  extends: [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:react-hooks/recommended\",\n  ],\n  ignorePatterns: [\"dist\", \".eslintrc.cjs\"],\n  parser: \"@typescript-eslint/parser\",\n  plugins: [\"react-refresh\"],\n  rules: {\n    \"react-refresh/only-export-components\": [\n      \"warn\",\n      { allowConstantExport: true },\n    ],\n  },\n};\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n/test-results/\n/playwright-report/\n/playwright/.cache/\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n   <a href=\"https://devresume.app\" target=\"_blank\">\n    <img src=\"screenshot.png\" alt=\"Devices preview\" />\n  </a>\n</p>\n<h1 align=\"center\">DevResume</h1>\n\n<div align=\"center\">\n\n  <h3>Resume creator based on writing YAML with live preview and PDF export.</h3>\n  \n<br />\n\nWebsite: https://devresume.app\n\n**Completely free** • **No sign-up** • **Live preview** • **Works offline** • **Unlimited exports**\n\n</div>\n\n## Motivation\n\nDevResume helps developers and technical people write their resume instead of wrestling with buttons and menus. \nThe configuration is based on a subset of the [JSON resume standard](https://jsonresume.org/). It includes only the actually useful parts of it. The best practices are already taken care of based on the recommendations from the book [The Tech Resume Inside Out](https://thetechresume.com/). You are free to focus on the content. \n\n## Scripts\n\nIn the project directory, you can run:\n\n#### `npm run dev`\nRuns the app in the development mode.\\\nOpen [http://127.0.0.1:5173/](http://127.0.0.1:5173/) to view it in the browser.\n\n#### `npm run build`\nBuilds the app for production in the `dist` folder.\n\n#### `npm run test`\nRuns the unit and integrations tests using Vitest.\n\n#### `npm run test:e2e`\nRuns the E2E tests using Playwright.\n\n\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/index.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  }\n}"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n    <link rel=\"manifest\" href=\"/site.webmanifest\" />\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>DevResume</title>\n  </head>\n  <body spellcheck=\"false\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"devresume\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite --host\",\n    \"build\": \"tsc && vite build\",\n    \"lint\": \"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0\",\n    \"preview\": \"vite preview\",\n    \"test\": \"vitest\",\n    \"test:e2e\": \"npx playwright test\"\n  },\n  \"dependencies\": {\n    \"@codemirror/language\": \"^6.6.0\",\n    \"@codemirror/legacy-modes\": \"^6.3.1\",\n    \"@codemirror/lint\": \"^6.2.0\",\n    \"@codemirror/stream-parser\": \"^0.19.9\",\n    \"@react-hook/resize-observer\": \"^1.2.6\",\n    \"@react-pdf/renderer\": \"3.1.12\",\n    \"@uiw/codemirror-theme-github\": \"^4.19.9\",\n    \"@uiw/codemirror-theme-vscode\": \"^4.21.18\",\n    \"@uiw/react-codemirror\": \"^4.19.9\",\n    \"markdown-to-jsx\": \"^7.3.2\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-pdf\": \"7.3.3\",\n    \"split-pane-react\": \"^0.1.3\",\n    \"yaml\": \"^2.3.2\",\n    \"z-schema\": \"^6.0.1\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.39.0\",\n    \"@testing-library/react\": \"^14.0.0\",\n    \"@types/codemirror\": \"^5.60.10\",\n    \"@types/node\": \"^20.8.8\",\n    \"@types/react\": \"^18.2.15\",\n    \"@types/react-dom\": \"^18.2.7\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.0.0\",\n    \"@typescript-eslint/parser\": \"^6.0.0\",\n    \"@vitejs/plugin-react\": \"^4.0.3\",\n    \"eslint\": \"^8.45.0\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.3\",\n    \"jsdom\": \"^22.1.0\",\n    \"typescript\": \"^5.0.2\",\n    \"vite\": \"^4.4.5\",\n    \"vitest\": \"^0.34.6\"\n  }\n}\n"
  },
  {
    "path": "playwright/components/controls.ts",
    "content": "import { Page, expect } from \"@playwright/test\";\n\nexport function PreviewControls(page: Page) {\n  const zoomIn = () => page.getByTestId(\"zoom-in\").click();\n  const zoomOut = () => page.getByTestId(\"zoom-out\").click();\n  const exportPDF = () => page.getByTestId(\"export\").click();\n\n  return {\n    zoomIn,\n    zoomOut,\n    exportPDF,\n  };\n}\n\nexport function TitleControls(page: Page) {\n  const input = page.getByTestId(\"title\");\n\n  const setTitle = (value: string) => input.fill(value);\n\n  return {\n    setTitle,\n    expect: () => ({\n      toHaveTitle: (title: string) => expect(input).toHaveValue(title),\n    }),\n  };\n}\n\nexport function FileControls(page: Page) {\n  const open = () => page.getByTestId(\"open\").click();\n  const save = () => page.getByTestId(\"save\").click();\n  const newResume = () => page.getByTestId(\"new\").click();\n\n  return {\n    open,\n    save,\n    newResume,\n  };\n}\n"
  },
  {
    "path": "playwright/components/editor.ts",
    "content": "import { Page, expect } from \"@playwright/test\";\n\nexport function Editor(page: Page) {\n  const self = page.locator(\".cm-content\");\n\n  const type = async (value: string) => {\n    await self.focus();\n    await page.keyboard.insertText(value);\n  };\n\n  const clearAndRefresh = async () => {\n    await page.evaluate(() => {\n      localStorage[\"yaml\"] = \"\";\n    });\n    await page.reload();\n  };\n\n  return {\n    type,\n    clearAndRefresh,\n    expect: () => ({\n      toHaveEmptyText: async () => {\n        await expect\n          .poll(async () => {\n            const text = await self.innerText();\n            return text.trim();\n          })\n          .toEqual(\"\");\n      },\n    }),\n  };\n}\n"
  },
  {
    "path": "playwright/components/index.ts",
    "content": "export * from \"./controls\";\nexport * from \"./pdf-document\";\nexport * from \"./editor\";\n"
  },
  {
    "path": "playwright/components/pdf-document.ts",
    "content": "import { Page, expect } from \"@playwright/test\";\n\nexport function PDFDocument(page: Page) {\n  // We need to check the data-ready attribute as well\n  // in order to avoid getting the back buffer\n  const self = page.locator('[data-testid=\"pdf-document\"][data-ready=\"true\"]');\n  const pages = page.locator(\".react-pdf__Page\");\n\n  const waitToAppear = () => self.waitFor({ state: \"visible\" });\n\n  const getScale = async () => {\n    const scale = await self.getAttribute(\"data-scale\");\n    return Number(scale);\n  };\n\n  const waitToZoomIn = async (initialScale: number) => {\n    expect.poll(getScale).toBeGreaterThan(initialScale);\n  };\n\n  const waitToZoomOut = async (initialScale: number) => {\n    expect.poll(getScale).toBeLessThan(initialScale);\n  };\n\n  return {\n    self,\n    waitToAppear,\n    getScale,\n    waitToZoomIn,\n    waitToZoomOut,\n    expect: () => ({\n      ...expect(self),\n      async toHaveScreenshotsOfPages() {\n        for (const page of await pages.all()) {\n          await expect(page).toHaveScreenshot({ scale: \"device\" });\n        }\n      },\n    }),\n  };\n}\n"
  },
  {
    "path": "playwright/controls.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { PDFDocument } from \"./components/pdf-document\";\nimport {\n  Editor,\n  FileControls,\n  PreviewControls,\n  TitleControls,\n} from \"./components\";\nimport { SAMPLE_YAML } from \"../src/parsing/sample\";\n\ntest.describe(\"preview controls\", () => {\n  let pdfDocument;\n\n  test.beforeEach(async ({ page }) => {\n    await page.goto(\"/\");\n    pdfDocument = PDFDocument(page);\n    await pdfDocument.waitToAppear();\n  });\n\n  test.describe(\"zooming\", () => {\n    let initialScale;\n\n    test.beforeEach(async () => {\n      initialScale = await pdfDocument.getScale();\n    });\n\n    test(\"should zoom-in\", async ({ page }) => {\n      await PreviewControls(page).zoomIn();\n      await pdfDocument.waitToZoomIn(initialScale);\n      await pdfDocument.expect().toHaveScreenshotsOfPages();\n    });\n\n    test(\"should zoom-out\", async ({ page }) => {\n      await PreviewControls(page).zoomOut();\n      await pdfDocument.waitToZoomOut(initialScale);\n      await pdfDocument.expect().toHaveScreenshotsOfPages();\n    });\n  });\n\n  test(\"should export pdf\", async ({ page }) => {\n    const title = \"TestTitle\";\n    await TitleControls(page).setTitle(title);\n    const downloadPromise = page.waitForEvent(\"download\");\n    await PreviewControls(page).exportPDF();\n    const download = await downloadPromise;\n\n    expect(download.suggestedFilename()).toBe(title + \".pdf\");\n  });\n});\n\ntest.describe(\"file controls\", () => {\n  test(\"should open .yml files\", async ({ page }) => {\n    await page.goto(\"/\");\n    await Editor(page).clearAndRefresh();\n\n    const fileChooserPromise = page.waitForEvent(\"filechooser\");\n    await FileControls(page).open();\n    const fileChooser = await fileChooserPromise;\n    await fileChooser.setFiles({\n      name: \"sample.yml\",\n      mimeType: \"text/yaml\",\n      buffer: Buffer.from(SAMPLE_YAML),\n    });\n\n    await TitleControls(page).expect().toHaveTitle(\"sample\");\n\n    const pdfDocument = PDFDocument(page);\n    await pdfDocument.waitToAppear();\n    await pdfDocument.expect().toHaveScreenshotsOfPages();\n  });\n\n  test(\"should save yaml\", async ({ page }) => {\n    await page.goto(\"/\");\n    const title = \"TestTitle\";\n    await TitleControls(page).setTitle(title);\n    const downloadPromise = page.waitForEvent(\"download\");\n    await FileControls(page).save();\n    const download = await downloadPromise;\n\n    expect(download.suggestedFilename()).toBe(title + \".yaml\");\n  });\n\n  test(\"should create new resumes\", async ({ page }) => {\n    await page.goto(\"/\");\n    await FileControls(page).newResume();\n\n    await Editor(page).expect().toHaveEmptyText();\n    await PDFDocument(page).expect().toBeHidden();\n  });\n});\n"
  },
  {
    "path": "playwright/sections.spec.ts",
    "content": "import { test } from \"@playwright/test\";\nimport { Editor, PDFDocument } from \"./components\";\nimport {\n  basicsYAML,\n  workYAML,\n  volunteerYAML,\n  educationYAML,\n  awardsYAML,\n  publicationsYAML,\n  skillsYAML,\n  projectsYAML,\n} from \"./yaml\";\n\nconst sections = [\n  { name: \"basics\", yaml: basicsYAML },\n  { name: \"work\", yaml: workYAML },\n  { name: \"volunteer\", yaml: volunteerYAML },\n  { name: \"education\", yaml: educationYAML },\n  { name: \"awards\", yaml: awardsYAML },\n  { name: \"publications\", yaml: publicationsYAML },\n  { name: \"skills\", yaml: skillsYAML },\n  { name: \"projects\", yaml: projectsYAML },\n];\n\nconst orders = [\n  {\n    name: \"default\",\n    sectionsOrder: [\n      \"basics\",\n      \"skills\",\n      \"work\",\n      \"projects\",\n      \"education\",\n      \"awards\",\n      \"certificates\",\n      \"publications\",\n      \"volunteer\",\n    ],\n  },\n  {\n    name: \"user-specified\",\n    sectionsOrder: [\n      \"basics\",\n      \"volunteer\",\n      \"work\",\n      \"projects\",\n\n      \"awards\",\n      \"certificates\",\n      \"skills\",\n      \"publications\",\n      \"education\",\n    ],\n  },\n];\n\nfor (const { name, yaml } of sections) {\n  test(`should render section: ${name}`, async ({ page }) => {\n    await page.goto(\"/\");\n\n    const editor = Editor(page);\n    await editor.clearAndRefresh();\n    await editor.type(yaml);\n\n    const document = PDFDocument(page);\n    await document.waitToAppear();\n    await document.expect().toHaveScreenshotsOfPages();\n  });\n}\n\nfor (const { name, sectionsOrder } of orders) {\n  test(`should render sections in the specified order: ${name}`, async ({\n    page,\n  }) => {\n    await page.goto(\"/\");\n\n    const editor = Editor(page);\n    await editor.clearAndRefresh();\n\n    for (const { yaml } of sections) {\n      await editor.type(yaml);\n    }\n\n    await editor.type(\n      `meta:\\n  sectionsOrder:\\n${sectionsOrder\n        .map((sectionName) => `    - ${sectionName}`)\n        .join(\"\\n\")}`\n    );\n\n    const document = PDFDocument(page);\n    await document.waitToAppear();\n    await document.expect().toHaveScreenshotsOfPages();\n  });\n}\n\ntest(`should each section in sectionsPageBreaks on a new page`, async ({\n  page,\n}) => {\n  await page.goto(\"/\");\n\n  const editor = Editor(page);\n  await editor.clearAndRefresh();\n\n  for (const { yaml } of sections) {\n    await editor.type(yaml);\n  }\n\n  await editor.type(\n    `meta:\\n  sectionsPageBreaks:\\n${sections\n      .map(({ name }) => `    - ${name}`)\n      .join(\"\\n\")}`\n  );\n\n  const document = PDFDocument(page);\n  await document.waitToAppear();\n  await document.expect().toHaveScreenshotsOfPages();\n});\n"
  },
  {
    "path": "playwright/yaml.ts",
    "content": "export const basicsYAML = `\nbasics:\n  name: Name1 Name2\n  label: Label\n  email: email@test.com\n  phone: (912) 555-4321\n  url: http://url.test.com\n  summary: |\n    Summary Line1\n    Summary Line2\n  location:\n    city: City\n    countryCode: Country\n  profiles:\n    - network: github\n      url: github.com/test\n    - network: linkedin\n      url: linkedin.com/test\n`;\n\nexport const workYAML = `\nwork:\n  - name: Work1\n    position: Position1\n    location: Location1\n    url: http://url1.example.com\n    summary: \n      Summary1 **summary bold** *symmary italic* [summary link](https://link.test.com).\n    startDate: 2010-12\n    endDate: 2010-01\n    highlights: \n      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).\n      - Highlight 2.2 that takes more space to test what happens with the resume in this case. Some more text to force new lines in the pdf.\n\n  - name: Work2 \n    position: Position2\n    location: Location2\n    url: http://url2.example.com\n    summary: \n      Summary2 **summary bold** *symmary italic* [summary link](https://link.test.com).\n    startDate: 2009-12\n    endDate: 2009-01\n    highlights: \n      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).\n      - Highlight 2.2 that takes more space to test what happens with the resume in this case. Some more text to force new lines in the pdf.\n`;\n\nexport const volunteerYAML = `\nvolunteer:\n  - organization: Organization1\n    position: Position1\n    url: http://url1.example.com/\n    startDate: 2010-12\n    endDate: 2010-01\n    highlights:\n      - Highlight 1.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).\n      - Highlight 1.2 that takes more space to test what happens with the resume in this case. Some more text to force new lines in the pdf.\n\n  - organization: Organization2\n    position: Position2\n    url: http://url2.example.com/\n    startDate: 2009-12\n    endDate: 2009-01\n    highlights:\n      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).\n      - Highlight 2.2 that takes more space to test what happens with the resume in this case. Some more text to force new lines in the pdf.\n`;\n\nexport const educationYAML = `\neducation:\n  - institution: Institution1\n    url: https://url1.test.com/\n    area: Area1\n    score: Score1 **score bold** *score italic* [score link](https://link.test.com).\n    startDate: 2010-12\n    endDate: 2010-01\n    courses:\n      - Course 1.1 **course bold** *course italic* [course link](https://link.test.com).\n      - Course 1.2 that takes more space to test what happens with the resume in this case. Some more text to force new lines in the pdf.\n  \n  - institution: Institution2\n    url: https://url2.test.com/\n    area: Area2\n    score: Score2 **score bold** *score italic* [score link](https://link.test.com).\n    startDate: 2009-12\n    endDate: 2009-01\n    courses:\n      - Course 2.1 **course bold** *course italic* [course link](https://link.test.com).\n      - Course 2.2 that takes more space to test what happens with the resume in this case. Some more text to force new lines in the pdf.\n`;\n\nexport const awardsYAML = `\nawards:\n  - title: Award1\n    date: 2014-11\n    awarder: Awarder1 \n    summary: Summary 1 **summary bold** *summary italic* [summary link](https://link.test.com).\n\n  - title: Award2\n    date: 2012-02\n    awarder: Awarder2\n    summary: Summary 2 **summary bold** *summary italic* [summary link](https://link.test.com).\n`;\n\nexport const publicationsYAML = `\npublications:\n  - name: Name1\n    publisher: Publiser1\n    releaseDate: 2014-12-06\n    url: http://url1.test.com\n    summary: Summary 1 **summary bold** *summary italic* [summary link](https://link.test.com).\n   \n  - name: Name2\n    publisher: Publiser2\n    releaseDate: 2010-10-10\n    url: http://url2.test.com\n    summary: Summary 2 **summary bold** *summary italic* [summary link](https://link.test.com).\n`;\n\nexport const skillsYAML = `\nskills:\n  - name: Category1\n    keywords: [\"Skill 1.1\", \"Skill 1.2\"]\n    \n  - name: Category2\n    keywords: [\"Skill 2.1\"]\n`;\n\nexport const projectsYAML = `\nprojects: \n  - name: Project1\n    description: Description 1 **description bold** *description italic* [description link](https://link.test.com).\n    startDate: 2010-12\n    endDate: 2010-01\n    url: url1.example.com\n    highlights:\n      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).\n      - Highlight 2.2 that takes more space to test what happens with the resume in this case. Some more text to force new lines in the pdf.\n\n  - name: Project2\n    description: Description 2 **description bold** *description italic* [description link](https://link.test.com).\n    startDate: 2009-12\n    endDate: 2009-01\n    url: url1.example.com\n    highlights:\n      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).\n      - Highlight 2.2 that takes more space to test what happens with the resume in this case. Some more text to force new lines in the pdf.\n`;\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// require('dotenv').config();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\n\nconst DEVICE_OVERRIDES = {\n  viewport: {\n    width: 1500,\n    height: 1000,\n  },\n  deviceScaleFactor: 2,\n};\n\nexport default defineConfig({\n  testDir: \"./playwright\",\n\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n\n  retries: 0,\n\n  workers: 3,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: \"html\",\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    baseURL: \"http://127.0.0.1:5173/\",\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: \"on-first-retry\",\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    {\n      name: \"chromium\",\n      use: {\n        ...devices[\"Desktop Chrome\"],\n        ...DEVICE_OVERRIDES,\n      },\n    },\n\n    {\n      name: \"firefox\",\n      use: {\n        ...devices[\"Desktop Firefox\"],\n        ...DEVICE_OVERRIDES,\n      },\n    },\n\n    {\n      name: \"webkit\",\n      use: {\n        ...devices[\"Desktop Safari\"],\n        ...DEVICE_OVERRIDES,\n      },\n    },\n  ],\n});\n"
  },
  {
    "path": "public/cache-sw.js",
    "content": "self.addEventListener(\"activate\", (event) => {\n  console.log(\"SW: Activate\", event);\n  event.waitUntil(self.clients.claim());\n});\n\nself.addEventListener(\"install\", () => {\n  console.log(\"SW: Install\");\n});\n\nconst putInCache = async (request, response) => {\n  const cache = await caches.open(\"v1\");\n  await cache.put(request, response);\n};\n\nconst cacheFirst = async (request) => {\n  const responseFromCache = await caches.match(request);\n  if (responseFromCache) {\n    return responseFromCache;\n  }\n  const responseFromNetwork = await fetch(request);\n  putInCache(request, responseFromNetwork.clone());\n  return responseFromNetwork;\n};\n\nself.addEventListener(\"fetch\", (event) => {\n  if (event.request.url.includes(\"pdf.worker\")) {\n    event.respondWith(cacheFirst(event.request));\n  }\n});\n"
  },
  {
    "path": "public/site.webmanifest",
    "content": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n       {\n          \"src\": \"/android-chrome-192x192.png\",\n          \"sizes\": \"192x192\",\n          \"type\": \"image/png\"\n       },\n       {\n        \"src\": \"/android-chrome-512x512.png\",\n        \"sizes\": \"512x512\",\n        \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#FFFFFF\",\n    \"background_color\": \"#FFFFFF\",\n    \"display\": \"standalone\"\n }"
  },
  {
    "path": "src/App.tsx",
    "content": "import { RefObject, useCallback, useRef, useState } from \"react\";\nimport { PDF, useRender, useScale } from \"./rendering\";\nimport { Schema, YAMLEditor } from \"./editing\";\nimport \"split-pane-react/esm/themes/default.css\";\nimport {\n  ControlsLayout,\n  FileControls,\n  PreviewControls,\n  TitleControls,\n} from \"./controls\";\nimport { PanesLayout } from \"./panes-layout\";\nimport \"./app.css\";\nimport { useYAMLPersistence, downloadFile } from \"./persistence\";\nimport { useYAMLParsing } from \"./parsing\";\nimport { ReactCodeMirrorRef } from \"@uiw/react-codemirror\";\n\nexport function App() {\n  const { queue, blob, setBlob } = useRender();\n  const { zoomIn, zoomOut, scale, maxScaleReached, minScaleReached } = useScale(\n    { minScale: 0.5, maxScale: 2 }\n  );\n  const [title, setTitle] = useState(\"Untitled\");\n  const codeMirrorRef: RefObject<ReactCodeMirrorRef> = useRef(null);\n\n  // Parsing\n  const onYAMLParsed = useCallback(\n    (yaml: string, json: object | undefined) => {\n      if (json) queue.push(json);\n      else if (!yaml) queue.clear();\n    },\n    [queue]\n  );\n  const { setYAML, yaml } = useYAMLParsing({ onYAMLParsed });\n\n  // Persistence\n  const onFileOpened = useCallback(\n    (fileTitle: string, fileContents: string) => {\n      setTitle(fileTitle);\n      setYAML(fileContents);\n    },\n    [setYAML]\n  );\n  const { save, open } = useYAMLPersistence({\n    title,\n    yaml: yaml,\n    onFileOpened,\n  });\n\n  // Export\n  const onDownload = useCallback(() => {\n    if (blob) {\n      downloadFile(title, blob);\n    }\n  }, [blob, title]);\n\n  const onNewResume = useCallback(() => {\n    setTitle(\"Untitled\");\n    setYAML(\"\");\n    setBlob(null);\n\n    if (codeMirrorRef.current && codeMirrorRef.current.view) {\n      codeMirrorRef.current.view.focus();\n    }\n  }, [setYAML, setBlob]);\n\n  return (\n    <div className=\"App\">\n      <ControlsLayout\n        left={<FileControls onOpen={open} onSave={save} onNew={onNewResume} />}\n        center={<TitleControls title={title} onChange={setTitle} />}\n        right={\n          <PreviewControls\n            onDownload={onDownload}\n            zoomInDisabled={maxScaleReached}\n            zoomOutDisabled={minScaleReached}\n            onZoomIn={zoomIn}\n            onZoomOut={zoomOut}\n          />\n        }\n      />\n\n      <PanesLayout\n        left={\n          <YAMLEditor\n            value={yaml}\n            onChange={setYAML}\n            codeMirrorRef={codeMirrorRef}\n          />\n        }\n        right={<PDF scale={scale} blob={blob} />}\n        bottom={<Schema />}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app.css",
    "content": ".App {\n  display: flex;\n  position: relative;\n  width: 100vw;\n  height: 100vh;\n  flex-direction: column;\n}\n"
  },
  {
    "path": "src/controls/controls-layout.css",
    "content": ".ControlsLayout {\n  display: flex;\n  background-color: var(--color-gray-100);\n  border-bottom: 1px solid var(--color-gray-400);\n}\n"
  },
  {
    "path": "src/controls/controls-layout.tsx",
    "content": "import { ReactElement, cloneElement } from \"react\";\nimport \"./controls-layout.css\";\n\ntype Props = {\n  left: ReactElement;\n  center: ReactElement;\n  right: ReactElement;\n};\n\nexport function ControlsLayout({ left, center, right }: Props) {\n  return (\n    <div className=\"ControlsLayout\">\n      {cloneElement(left, { style: { flex: 1 } })}\n      {cloneElement(center, { style: { flex: 1 } })}\n      {cloneElement(right, { style: { flex: 1 } })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/controls/file-controls.css",
    "content": ".FileControls {\n  gap: var(--space-3);\n  display: flex;\n  padding: var(--space-6);\n  color: white;\n}\n"
  },
  {
    "path": "src/controls/file-controls.tsx",
    "content": "import { CSSProperties } from \"react\";\nimport \"./file-controls.css\";\nimport { DownloadIcon, FolderIcon, InfoIcon, PlusIcon } from \"../icons\";\n\ntype Props = {\n  onSave: () => void;\n  onOpen: () => void;\n  onNew: () => void;\n  style?: CSSProperties;\n};\n\nexport function FileControls({ onSave, onOpen, onNew, style }: Props) {\n  return (\n    <div className=\"FileControls\" style={style}>\n      <a\n        title=\"About\"\n        data-testin=\"about\"\n        target=\"_blank\"\n        href=\"https://github.com/vangelov/devresume\"\n      >\n        <InfoIcon size={16} />\n      </a>\n\n      <button data-testid=\"save\" onClick={onSave}>\n        <DownloadIcon size={14} style={{ marginRight: \"0.5rem\" }} />\n        Save\n      </button>\n\n      <button data-testid=\"open\" onClick={onOpen}>\n        <FolderIcon size={14} style={{ marginRight: \"0.5rem\" }} />\n        Open\n      </button>\n\n      <button data-testid=\"new\" onClick={onNew}>\n        <PlusIcon size={14} style={{ marginRight: \"0.5rem\" }} />\n        New\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/controls/index.ts",
    "content": "export * from \"./controls-layout\";\nexport * from \"./preview-controls\";\nexport * from \"./title-controls\";\nexport * from \"./file-controls\";\n"
  },
  {
    "path": "src/controls/preview-controls.css",
    "content": ".PreviewControls {\n  display: flex;\n  gap: var(--space-3);\n  justify-content: flex-end;\n  padding: var(--space-6);\n}\n"
  },
  {
    "path": "src/controls/preview-controls.tsx",
    "content": "import { CSSProperties } from \"react\";\nimport \"./preview-controls.css\";\nimport { ZoomOutIcon, PDFIcon, ZoomInIcon } from \"../icons\";\n\ntype Props = {\n  zoomInDisabled: boolean;\n  zoomOutDisabled: boolean;\n  onZoomIn: () => void;\n  onZoomOut: () => void;\n  onDownload: () => void;\n  style?: CSSProperties;\n};\n\nexport function PreviewControls({\n  zoomInDisabled,\n  zoomOutDisabled,\n  onZoomIn,\n  onZoomOut,\n  onDownload,\n  style,\n}: Props) {\n  return (\n    <div className=\"PreviewControls\" style={style}>\n      <button\n        disabled={zoomOutDisabled}\n        data-testid=\"zoom-out\"\n        title=\"Zoom out\"\n        onClick={onZoomOut}\n      >\n        <ZoomOutIcon size={16} />\n      </button>\n\n      <button\n        disabled={zoomInDisabled}\n        data-testid=\"zoom-in\"\n        title=\"Zoom in\"\n        onClick={onZoomIn}\n      >\n        <ZoomInIcon size={16} />\n      </button>\n\n      <button className=\"primary\" data-testid=\"export\" onClick={onDownload}>\n        <PDFIcon size={14} style={{ marginRight: \"0.5rem\" }} />\n        Export\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/controls/title-controls.css",
    "content": ".TitleControls {\n  height: 100%;\n  display: flex;\n  justify-content: center;\n  overflow-x: hidden;\n  align-items: center;\n}\n\n.TitleControls-Input {\n  width: 90%;\n  color: white;\n  font-weight: 600;\n  background-color: transparent;\n  border: none;\n  text-align: center;\n  font-size: var(--font-size-3);\n}\n\n.TitleControls-Input:focus {\n  outline: var(--color-blue-100);\n  outline-style: solid;\n  outline-width: var(--space-1);\n}\n"
  },
  {
    "path": "src/controls/title-controls.tsx",
    "content": "import { CSSProperties } from \"react\";\nimport \"./title-controls.css\";\n\ntype Props = {\n  title: string;\n  onChange: (value: string) => void;\n  style?: CSSProperties;\n};\n\nexport function TitleControls({ title, onChange, style }: Props) {\n  return (\n    <div className=\"TitleControls\" style={style}>\n      <input\n        className=\"TitleControls-Input\"\n        data-testid=\"title\"\n        value={title}\n        onChange={(event) => onChange(event.target.value)}\n        onBlur={(event) => {\n          if (!event.target.value) {\n            onChange(\"Untitled\");\n          }\n        }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/documents/bar.tsx",
    "content": "import { View, StyleSheet } from \"@react-pdf/renderer\";\nimport { Theme } from \"./theme\";\nimport { useMemo } from \"react\";\n\ntype Props = {\n  theme: Theme;\n};\n\nfunction createStyles(theme: Theme) {\n  return StyleSheet.create({\n    root: {\n      backgroundColor: theme.color.accent,\n      height: theme.space[4],\n      position: \"absolute\",\n      left: 0,\n      right: 0,\n      top: 0,\n    },\n  });\n}\n\nexport function Bar({ theme }: Props) {\n  const styles = useMemo(() => createStyles(theme), [theme]);\n  return <View fixed style={styles.root} />;\n}\n"
  },
  {
    "path": "src/documents/document.tsx",
    "content": "import { Resume } from \"../types\";\nimport { Page, Document, StyleSheet } from \"@react-pdf/renderer\";\nimport { BasicsSection } from \"./sections/basics-section\";\nimport {\n  AwardsSection,\n  CertificatesSection,\n  EducationSection,\n  ProjectsSection,\n  PublicationsSection,\n  SkillsSection,\n  VolunteerSection,\n  WorkSection,\n} from \"./sections\";\nimport { Theme, createTheme } from \"./theme\";\nimport { Bar } from \"./bar\";\nimport { useMemo } from \"react\";\nimport { getSectionsOrder } from \"./utils\";\nimport { SectionProps } from \"./section\";\nimport { useHasPageBreak } from \"./use-has-page-break\";\n\ntype Props = {\n  resume: Resume;\n};\n\nfunction createStyles(theme: Theme) {\n  return StyleSheet.create({\n    page: {\n      backgroundColor: \"white\",\n      fontFamily: \"Roboto\",\n      paddingVertical: theme.space[10],\n      paddingHorizontal: theme.space[12],\n      fontSize: theme.fontSize[0],\n      lineHeight: theme.lineHeight,\n      color: theme.color.text,\n    },\n  });\n}\n\nexport function ResumeDocument({ resume }: Props) {\n  const {\n    basics,\n    work,\n    skills,\n    projects,\n    education,\n    awards,\n    certificates,\n    publications,\n    volunteer,\n    meta,\n  } = resume;\n\n  const accentColor = meta && meta.accentColor;\n  const baseFontSize = meta && meta.baseFontSize;\n  const sectionsOrder = useMemo(() => getSectionsOrder(meta), [meta]);\n\n  const theme = useMemo(\n    () => createTheme(accentColor, baseFontSize),\n    [accentColor, baseFontSize]\n  );\n  const styles = createStyles(theme);\n\n  const hasPageBreak = useHasPageBreak(meta);\n\n  return (\n    <Document>\n      <Page style={styles.page} size=\"A4\">\n        <Bar theme={theme} />\n\n        {sectionsOrder.map((sectionName) => {\n          const commonProps: SectionProps = {\n            theme,\n            hasPageBreak: hasPageBreak(sectionName),\n          };\n\n          if (sectionName === \"basics\" && basics) {\n            return (\n              <BasicsSection\n                {...commonProps}\n                key={sectionName}\n                basics={basics}\n              />\n            );\n          }\n\n          if (sectionName === \"skills\" && Array.isArray(skills)) {\n            return (\n              <SkillsSection\n                {...commonProps}\n                key={sectionName}\n                skills={skills}\n              />\n            );\n          }\n\n          if (sectionName === \"work\" && Array.isArray(work)) {\n            return (\n              <WorkSection {...commonProps} key={sectionName} work={work} />\n            );\n          }\n\n          if (sectionName === \"projects\" && Array.isArray(projects)) {\n            return (\n              <ProjectsSection\n                {...commonProps}\n                key={sectionName}\n                projects={projects}\n              />\n            );\n          }\n\n          if (sectionName === \"education\" && Array.isArray(education)) {\n            return (\n              <EducationSection\n                {...commonProps}\n                key={sectionName}\n                education={education}\n              />\n            );\n          }\n\n          if (sectionName === \"awards\" && Array.isArray(awards)) {\n            return (\n              <AwardsSection\n                {...commonProps}\n                key={sectionName}\n                awards={awards}\n              />\n            );\n          }\n\n          if (sectionName === \"certificates\" && Array.isArray(certificates)) {\n            return (\n              <CertificatesSection\n                {...commonProps}\n                key={sectionName}\n                certificates={certificates}\n              />\n            );\n          }\n\n          if (sectionName === \"publications\" && Array.isArray(publications)) {\n            return (\n              <PublicationsSection\n                {...commonProps}\n                key={sectionName}\n                publications={publications}\n              />\n            );\n          }\n\n          if (sectionName === \"volunteer\" && Array.isArray(volunteer)) {\n            return (\n              <VolunteerSection\n                {...commonProps}\n                key={sectionName}\n                volunteer={volunteer}\n              />\n            );\n          }\n\n          return null;\n        })}\n      </Page>\n    </Document>\n  );\n}\n"
  },
  {
    "path": "src/documents/events-section.tsx",
    "content": "import { Text, Link } from \"@react-pdf/renderer\";\nimport { Section, SectionProps } from \"./section\";\nimport { HStack, VStack } from \"./stack\";\nimport { RichText } from \"./rich-text\";\nimport { Fragment, ReactElement, ReactNode } from \"react\";\nimport { formatDate } from \"./utils\";\nimport { Theme } from \"./theme\";\n\ntype EventHighlightItemProps = {\n  children: string;\n};\n\nexport function EventHighlightItem({ children }: EventHighlightItemProps) {\n  return (\n    <HStack wrap={false} style={{ alignItems: \"flex-start\" }} gap={3}>\n      <Text>•</Text>\n      <RichText>{children}</RichText>\n    </HStack>\n  );\n}\n\n//\n\ntype EventItemProps = {\n  title?: string;\n  url?: string;\n  titleDetails?: Array<ReactElement>;\n  description?: string;\n  children?: ReactNode;\n  startDate?: string | number;\n  endDate?: string | number;\n  date?: string | number;\n  theme: Theme;\n};\n\nexport function EventItem({\n  title,\n  url,\n  children,\n  description,\n  titleDetails,\n  startDate,\n  endDate,\n  date,\n  theme,\n}: EventItemProps) {\n  return (\n    <VStack wrap={false} gap={theme.space[2]}>\n      <VStack gap={theme.space[1]}>\n        <HStack\n          style={{\n            justifyContent: \"space-between\",\n            alignItems: \"flex-start\",\n          }}\n        >\n          <Text\n            style={{\n              marginRight: theme.space[5],\n              flex: 1,\n            }}\n          >\n            {url ? (\n              <Link\n                src={url}\n                style={{\n                  fontWeight: \"medium\",\n                  color: theme.color.text,\n                  textDecoration: \"none\",\n                }}\n              >\n                {title}\n              </Link>\n            ) : (\n              <Text style={{ fontWeight: \"medium\" }}>{title}</Text>\n            )}\n\n            {titleDetails &&\n              titleDetails.map((titleDetail, index) => (\n                <Fragment key={index}>\n                  <Text style={{ color: theme.color.lightText }}> • </Text>\n                  {titleDetail}\n                </Fragment>\n              ))}\n          </Text>\n\n          <Text style={{ color: theme.color.lightText }}>\n            {date ? (\n              formatDate(date)\n            ) : startDate || endDate ? (\n              <>\n                {startDate && formatDate(startDate)}\n                {endDate ? ` - ${formatDate(endDate)}` : \" - Present\"}\n              </>\n            ) : null}\n          </Text>\n        </HStack>\n\n        {description && <RichText>{description}</RichText>}\n      </VStack>\n\n      {children && <VStack gap={theme.space[2]}>{children}</VStack>}\n    </VStack>\n  );\n}\n\n//\n\nexport type EventsSectionProps = SectionProps;\n\nexport function EventsSection({\n  title,\n  children,\n  theme,\n  ...rest\n}: EventsSectionProps) {\n  return (\n    <Section theme={theme} title={title} {...rest}>\n      <VStack gap={theme.space[8]}>{children}</VStack>\n    </Section>\n  );\n}\n"
  },
  {
    "path": "src/documents/fonts.ts",
    "content": "import { Font } from \"@react-pdf/renderer\";\n\nFont.register({\n  family: \"Roboto\",\n  fonts: [\n    {\n      src: \"RobotoRegular.ttf\",\n      fontWeight: \"normal\",\n    },\n\n    {\n      src: \"RobotoMedium.ttf\",\n      fontWeight: \"medium\",\n    },\n\n    {\n      src: \"RobotoItalic.ttf\",\n      fontStyle: \"italic\",\n    },\n  ],\n});\n"
  },
  {
    "path": "src/documents/grouped-section.tsx",
    "content": "import { Text, View } from \"@react-pdf/renderer\";\nimport { Section } from \"./section\";\nimport { HStack, VStack } from \"./stack\";\nimport { RichText } from \"./rich-text\";\nimport { EventsSectionProps } from \"./events-section\";\n\ntype GroupItemProps = {\n  title?: string;\n  description?: string;\n};\n\nexport function GroupItem({ title, description }: GroupItemProps) {\n  return (\n    <HStack wrap={false} style={{ alignItems: \"flex-start\" }}>\n      {title && (\n        <Text style={{ flex: 0.6, fontWeight: \"medium\" }}>{title}</Text>\n      )}\n\n      {description ? (\n        <View\n          style={{\n            flex: 1,\n            display: \"flex\",\n          }}\n        >\n          <RichText>{description}</RichText>\n        </View>\n      ) : null}\n    </HStack>\n  );\n}\n\n//\n\nexport type GroupedSectionProps = EventsSectionProps;\n\nexport function GroupedSection({\n  title,\n  children,\n  theme,\n  ...rest\n}: GroupedSectionProps) {\n  return (\n    <Section theme={theme} title={title} {...rest}>\n      <VStack gap={theme.space[7]}>{children}</VStack>\n    </Section>\n  );\n}\n"
  },
  {
    "path": "src/documents/icons/github-icon.tsx",
    "content": "import { SvgIcon, SvgIconProps } from \"../svg-icon\";\nimport { Path } from \"@react-pdf/renderer\";\n\nexport function GitHubIcon({ color, ...rest }: SvgIconProps) {\n  return (\n    <SvgIcon {...rest}>\n      <Path\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        stroke={color}\n        d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\"\n      />\n    </SvgIcon>\n  );\n}\n"
  },
  {
    "path": "src/documents/icons/globe-icon.tsx",
    "content": "import { Circle, Line, Path } from \"@react-pdf/renderer\";\nimport { SvgIcon, SvgIconProps } from \"../svg-icon\";\n\nexport function GlobeIcon({ color, ...rest }: SvgIconProps) {\n  return (\n    <SvgIcon {...rest}>\n      <Circle\n        strokeWidth={2}\n        stroke={color}\n        strokeLineCap=\"round\"\n        cx=\"12\"\n        cy=\"12\"\n        r=\"10\"\n      />\n      <Line\n        strokeWidth={2}\n        stroke={color}\n        strokeLineCap=\"round\"\n        x1=\"2\"\n        y1=\"12\"\n        x2=\"22\"\n        y2=\"12\"\n      />\n      <Path\n        strokeWidth={2}\n        stroke={color}\n        strokeLineCap=\"round\"\n        d=\"M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\"\n      />\n    </SvgIcon>\n  );\n}\n"
  },
  {
    "path": "src/documents/icons/index.ts",
    "content": "export * from \"./mail-icon\";\nexport * from \"./location-icon\";\nexport * from \"./github-icon\";\nexport * from \"./globe-icon\";\nexport * from \"./phone-icon\";\nexport * from \"./linkedin-icon\";\nexport * from \"./map-pin-icon\";\n"
  },
  {
    "path": "src/documents/icons/linkedin-icon.tsx",
    "content": "import { Circle, Path, Rect } from \"@react-pdf/renderer\";\nimport { SvgIcon, SvgIconProps } from \"../svg-icon\";\n\nexport function LinkedInIcon({ color, ...rest }: SvgIconProps) {\n  return (\n    <SvgIcon {...rest}>\n      <Path\n        stroke={color}\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        d=\"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z\"\n      />\n      <Rect\n        stroke={color}\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        x=\"2\"\n        y=\"9\"\n        width=\"4\"\n        height=\"12\"\n      />\n      <Circle\n        stroke={color}\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        cx=\"4\"\n        cy=\"4\"\n        r=\"2\"\n      />\n    </SvgIcon>\n  );\n}\n"
  },
  {
    "path": "src/documents/icons/location-icon.tsx",
    "content": "import { Path, Circle } from \"@react-pdf/renderer\";\nimport { SvgIcon, SvgIconProps } from \"../svg-icon\";\n\nexport function LocationIcon({ color, ...rest }: SvgIconProps) {\n  return (\n    <SvgIcon {...rest}>\n      <Path\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        stroke={color}\n        d=\"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z\"\n      />\n      <Circle stroke={color} strokeWidth={2} cx=\"12\" cy=\"10\" r=\"3\"></Circle>\n    </SvgIcon>\n  );\n}\n"
  },
  {
    "path": "src/documents/icons/mail-icon.tsx",
    "content": "import { Path, Polyline } from \"@react-pdf/renderer\";\nimport { SvgIconProps } from \"../svg-icon\";\nimport { SvgIcon } from \"../svg-icon\";\n\nexport function MailIcon({ color, ...rest }: SvgIconProps) {\n  return (\n    <SvgIcon {...rest}>\n      <Path\n        stroke={color}\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        d=\"M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z\"\n      />\n      <Polyline\n        stroke={color}\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        points=\"22,6 12,13 2,6\"\n      />\n    </SvgIcon>\n  );\n}\n"
  },
  {
    "path": "src/documents/icons/map-pin-icon.tsx",
    "content": "import { Circle, Path } from \"@react-pdf/renderer\";\nimport { SvgIcon, SvgIconProps } from \"../svg-icon\";\n\nexport function MapPinIcon({ color, ...rest }: SvgIconProps) {\n  return (\n    <SvgIcon {...rest}>\n      <Path\n        strokeWidth={2}\n        stroke={color}\n        strokeLineCap=\"round\"\n        d=\"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z\"\n      />\n      <Circle\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        stroke={color}\n        cx=\"12\"\n        cy=\"10\"\n        r=\"3\"\n      />\n    </SvgIcon>\n  );\n}\n"
  },
  {
    "path": "src/documents/icons/phone-icon.tsx",
    "content": "import { SvgIcon, SvgIconProps } from \"../svg-icon\";\nimport { Path } from \"@react-pdf/renderer\";\n\nexport function PhoneIcon({ color, ...rest }: SvgIconProps) {\n  return (\n    <SvgIcon {...rest}>\n      <Path\n        stroke={color}\n        strokeWidth={2}\n        strokeLineCap=\"round\"\n        d=\"M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z\"\n      />\n    </SvgIcon>\n  );\n}\n"
  },
  {
    "path": "src/documents/index.ts",
    "content": "export * from \"./document\";\nexport * from \"./fonts\";\n"
  },
  {
    "path": "src/documents/rich-text.tsx",
    "content": "import { Text, Link } from \"@react-pdf/renderer\";\nimport Markdown from \"markdown-to-jsx\";\nimport { ReactNode } from \"react\";\n\ntype Props = {\n  children?: ReactNode;\n};\n\nconst TEXT_TYPES: Record<string, boolean | undefined> = {\n  em: true,\n  p: true,\n  span: true,\n  strong: true,\n};\n\nconst STYLE_FOR_TEXT: Record<string, object> = {\n  em: { fontStyle: \"italic\" },\n  strong: { fontWeight: \"medium\" },\n};\n\nexport function RichText({ children }: Props) {\n  return (\n    <Markdown\n      children={`\\n${children}`}\n      options={{\n        createElement(type, props, children) {\n          const isText = TEXT_TYPES[type as string];\n\n          if (isText) {\n            const style = STYLE_FOR_TEXT[type as string];\n\n            return (\n              <Text key={props.key} style={style}>\n                {children}\n              </Text>\n            );\n          } else if (type === \"a\" && \"href\" in props) {\n            return (\n              <Link\n                key={props.key}\n                src={props.href as string}\n                style={{ color: \"black\" }}\n              >\n                {children}\n              </Link>\n            );\n          }\n\n          return <Text key={props.key}>{children}</Text>;\n        },\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "src/documents/section.tsx",
    "content": "import { ReactNode, useMemo } from \"react\";\nimport { Text, View, StyleSheet } from \"@react-pdf/renderer\";\nimport { Theme } from \"./theme\";\n\nexport type SectionProps = {\n  title?: string;\n  children?: ReactNode;\n  theme: Theme;\n  hasPageBreak?: boolean;\n};\n\nfunction createStyles(theme: Theme) {\n  return StyleSheet.create({\n    title: {\n      fontSize: theme.fontSize[1],\n      color: theme.color.accent,\n      marginBottom: theme.space[5],\n      fontWeight: \"medium\",\n    },\n    root: {\n      marginBottom: theme.space[10],\n    },\n  });\n}\n\nexport function Section({\n  title,\n  children,\n  theme,\n  hasPageBreak,\n}: SectionProps) {\n  const styles = useMemo(() => createStyles(theme), [theme]);\n\n  return (\n    <View style={styles.root} break={hasPageBreak}>\n      {title && <Text style={styles.title}>{title}</Text>}\n      {children}\n    </View>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/awards-section.tsx",
    "content": "import { Award } from \"../../types\";\nimport { Text } from \"@react-pdf/renderer\";\nimport {\n  EventItem,\n  EventsSection,\n  EventsSectionProps,\n} from \"../events-section\";\nimport { ReactElement } from \"react\";\nimport { Theme } from \"../theme\";\n\nexport type AwardItemProps = {\n  award: Award;\n  theme: Theme;\n};\n\nexport function AwardItem({ award, theme }: AwardItemProps) {\n  const titleDetails: Array<ReactElement> = [];\n\n  if (award.awarder) {\n    titleDetails.push(<Text>{award.awarder}</Text>);\n  }\n\n  return (\n    <EventItem\n      theme={theme}\n      title={award.title}\n      description={award.summary}\n      titleDetails={titleDetails}\n      date={award.date}\n    />\n  );\n}\n\n//\n\nexport type AwardsSectionProps = {\n  awards: Array<Award | null>;\n} & EventsSectionProps;\n\nexport function AwardsSection({ awards, theme, ...rest }: AwardsSectionProps) {\n  return (\n    <EventsSection theme={theme} title=\"Awards\" {...rest}>\n      {awards.map(\n        (award, index) =>\n          award && <AwardItem key={index} theme={theme} award={award} />\n      )}\n    </EventsSection>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/basics-section/contacts.tsx",
    "content": "import { InfoItem } from \"./info-item\";\nimport {\n  GitHubIcon,\n  GlobeIcon,\n  LinkedInIcon,\n  MailIcon,\n  PhoneIcon,\n} from \"../../icons\";\nimport { VStack } from \"../../stack\";\nimport { Basics, Profile } from \"../../../types\";\nimport { Style } from \"@react-pdf/types\";\nimport { Theme } from \"../../theme\";\nimport { LocationInfoItem } from \"./location-info-item\";\n\nexport type Props = {\n  basics: Basics;\n  style: Style;\n  theme: Theme;\n};\n\nfunction getProfiles(basics: Basics) {\n  let linkedinProfile: Profile | null = null;\n  let githubProfile: Profile | null = null;\n\n  if (basics.profiles) {\n    for (const profile of basics.profiles) {\n      if (profile.network === \"github\") githubProfile = profile;\n      else if (profile.network === \"linkedin\") linkedinProfile = profile;\n    }\n  }\n\n  return { linkedinProfile, githubProfile };\n}\n\nexport function Contacts({ basics, style, theme }: Props) {\n  const { linkedinProfile, githubProfile } = getProfiles(basics);\n\n  return (\n    <VStack style={style} gap={2}>\n      {basics.phone && (\n        <InfoItem theme={theme} icon={<PhoneIcon />} value={basics.phone} />\n      )}\n      {basics.email && (\n        <InfoItem theme={theme} icon={<MailIcon />} value={basics.email} />\n      )}\n      {basics.url && (\n        <InfoItem theme={theme} icon={<GlobeIcon />} value={basics.url} />\n      )}\n      {githubProfile && githubProfile.url && (\n        <InfoItem\n          theme={theme}\n          icon={<GitHubIcon />}\n          value={githubProfile.url}\n          href={githubProfile.url}\n        />\n      )}\n      {linkedinProfile && linkedinProfile.url && (\n        <InfoItem\n          theme={theme}\n          icon={<LinkedInIcon />}\n          value={linkedinProfile.url}\n          href={linkedinProfile.url}\n        />\n      )}\n      {basics.location && (\n        <LocationInfoItem theme={theme} location={basics.location} />\n      )}\n    </VStack>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/basics-section/index.tsx",
    "content": "import { Text, View, StyleSheet } from \"@react-pdf/renderer\";\nimport { Section, SectionProps } from \"../../section\";\nimport { HStack, VStack } from \"../../stack\";\nimport { Basics } from \"../../../types\";\nimport { RichText } from \"../../rich-text\";\nimport { Contacts } from \"./contacts\";\nimport { Theme } from \"../../theme\";\nimport { useMemo } from \"react\";\n\nexport type Props = {\n  basics: Basics;\n} & SectionProps;\n\nfunction createStyles(theme: Theme) {\n  return StyleSheet.create({\n    name: { fontSize: theme.fontSize[2], fontWeight: \"medium\" },\n    label: {\n      color: theme.color.lightText,\n      fontSize: theme.fontSize[1],\n    },\n  });\n}\n\nexport function BasicsSection({ basics, theme, ...rest }: Props) {\n  const styles = useMemo(() => createStyles(theme), [theme]);\n\n  return (\n    <Section theme={theme} {...rest}>\n      <VStack gap={theme.space[5]}>\n        <HStack>\n          {basics.name && <Text style={styles.name}>{basics.name}</Text>}\n          {basics.label && (\n            <Text style={styles.label}>\n              <Text> </Text>• {basics.label}\n            </Text>\n          )}\n        </HStack>\n\n        <HStack style={{ alignItems: \"flex-start\" }}>\n          <Contacts theme={theme} style={{ flex: 0.6 }} basics={basics} />\n          <View style={{ flex: 1 }}>\n            {basics.summary && <RichText>{basics.summary}</RichText>}\n          </View>\n        </HStack>\n      </VStack>\n    </Section>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/basics-section/info-item.tsx",
    "content": "import { Text, Link, StyleSheet } from \"@react-pdf/renderer\";\nimport { ReactElement, cloneElement, useMemo } from \"react\";\nimport { HStack } from \"../../stack\";\nimport { Theme } from \"../../theme\";\n\ntype Props = {\n  icon: ReactElement;\n  value: string;\n  href?: string;\n  theme: Theme;\n};\n\nfunction createStyles(theme: Theme) {\n  return StyleSheet.create({\n    icon: {\n      size: theme.fontSize[0],\n      color: theme.color.lightText,\n      style: { marginRight: theme.space[2] },\n    },\n    link: { color: theme.color.lightText, textDecoration: \"none\" },\n  });\n}\n\nexport function InfoItem({ icon, href, value, theme }: Props) {\n  const styles = useMemo(() => createStyles(theme), [theme]);\n\n  return (\n    <HStack\n      style={{\n        color: theme.color.lightText,\n      }}\n    >\n      {cloneElement(icon, styles.icon)}\n      {href ? (\n        <Link style={styles.link} src={href}>\n          {value}\n        </Link>\n      ) : (\n        <Text>{value}</Text>\n      )}\n    </HStack>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/basics-section/location-info-item.tsx",
    "content": "import { Location } from \"../../../types\";\nimport { MapPinIcon } from \"../../icons\";\nimport { Theme } from \"../../theme\";\nimport { InfoItem } from \"./info-item\";\n\ntype Props = {\n  location: Location;\n  theme: Theme;\n};\n\nexport function LocationInfoItem({ location, theme }: Props) {\n  const parts = [];\n  const { city, countryCode } = location;\n\n  if (city) parts.push(city);\n  if (countryCode) parts.push(countryCode);\n\n  const value = parts.join(\", \");\n\n  return <InfoItem theme={theme} icon={<MapPinIcon />} value={value} />;\n}\n"
  },
  {
    "path": "src/documents/sections/certificates-section.tsx",
    "content": "import { Certificate } from \"../../types\";\nimport { Text } from \"@react-pdf/renderer\";\nimport { EventItem, EventsSection } from \"../events-section\";\nimport { ReactElement } from \"react\";\nimport { Theme } from \"../theme\";\nimport { SectionProps } from \"../section\";\n\nexport type CertificateItemProps = {\n  certificate: Certificate;\n  theme: Theme;\n};\n\nexport function CerticateItem({ certificate, theme }: CertificateItemProps) {\n  const titleDetails: Array<ReactElement> = [];\n\n  if (certificate.issuer) {\n    titleDetails.push(<Text>{certificate.issuer}</Text>);\n  }\n\n  return (\n    <EventItem\n      title={certificate.name}\n      theme={theme}\n      url={certificate.url}\n      titleDetails={titleDetails}\n      date={certificate.date}\n    />\n  );\n}\n\n//\n\ntype CertificatesSection = {\n  certificates: Array<Certificate | null>;\n} & SectionProps;\n\nexport function CertificatesSection({\n  theme,\n  certificates,\n  ...rest\n}: CertificatesSection) {\n  return (\n    <EventsSection theme={theme} title=\"Certificates\" {...rest}>\n      {certificates.map(\n        (certificate, index) =>\n          certificate && (\n            <CerticateItem\n              key={index}\n              theme={theme}\n              certificate={certificate}\n            />\n          )\n      )}\n    </EventsSection>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/education-section.tsx",
    "content": "import { EducationPlace } from \"../../types\";\nimport { Link } from \"@react-pdf/renderer\";\nimport {\n  EventHighlightItem,\n  EventItem,\n  EventsSection,\n  EventsSectionProps,\n} from \"../events-section\";\nimport { ReactElement } from \"react\";\nimport { Theme } from \"../theme\";\n\nexport type EducationPlaceItemProps = {\n  educationPlace: EducationPlace;\n  theme: Theme;\n};\n\nexport function EducationPlaceItem({\n  educationPlace,\n  theme,\n}: EducationPlaceItemProps) {\n  const titleDetails: Array<ReactElement> = [];\n\n  if (educationPlace.institution) {\n    titleDetails.push(\n      <Link\n        src={educationPlace.url || \"\"}\n        style={{ color: theme.color.text, textDecoration: \"none\" }}\n      >\n        {educationPlace.institution}\n      </Link>\n    );\n  }\n\n  return (\n    <EventItem\n      title={educationPlace.area}\n      description={educationPlace.score}\n      titleDetails={titleDetails}\n      startDate={educationPlace.startDate}\n      endDate={educationPlace.endDate}\n      theme={theme}\n    >\n      {educationPlace.courses &&\n        Array.isArray(educationPlace.courses) &&\n        educationPlace.courses.map(\n          (course) =>\n            course && (\n              <EventHighlightItem key={course}>{course}</EventHighlightItem>\n            )\n        )}\n    </EventItem>\n  );\n}\n\n//\n\ntype EducationSectionProps = {\n  education: Array<EducationPlace | null>;\n} & EventsSectionProps;\n\nexport function EducationSection({\n  education,\n  theme,\n  ...rest\n}: EducationSectionProps) {\n  return (\n    <EventsSection theme={theme} title=\"Education\" {...rest}>\n      {education.map(\n        (educationPlace, index) =>\n          educationPlace && (\n            <EducationPlaceItem\n              key={index}\n              theme={theme}\n              educationPlace={educationPlace}\n            />\n          )\n      )}\n    </EventsSection>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/index.ts",
    "content": "export * from \"./basics-section\";\nexport * from \"./work-section\";\nexport * from \"./education-section\";\nexport * from \"./skills-section\";\nexport * from \"./projects-section\";\nexport * from \"./awards-section\";\nexport * from \"./certificates-section\";\nexport * from \"./publications-section\";\nexport * from \"./volunteer-section\";\n"
  },
  {
    "path": "src/documents/sections/projects-section.tsx",
    "content": "import { Project } from \"../../types\";\nimport {\n  EventHighlightItem,\n  EventItem,\n  EventsSection,\n  EventsSectionProps,\n} from \"../events-section\";\nimport { Theme } from \"../theme\";\n\nexport type ProjectItemProps = {\n  project: Project;\n  theme: Theme;\n};\n\nexport function ProjectItem({ project, theme }: ProjectItemProps) {\n  return (\n    <EventItem\n      title={project.name}\n      url={project.url}\n      description={project.description}\n      startDate={project.startDate}\n      endDate={project.endDate}\n      theme={theme}\n    >\n      {project.highlights &&\n        Array.isArray(project.highlights) &&\n        project.highlights.map(\n          (hightlight) =>\n            hightlight && (\n              <EventHighlightItem key={hightlight}>\n                {hightlight}\n              </EventHighlightItem>\n            )\n        )}\n    </EventItem>\n  );\n}\n\n//\n\ntype ProjectsSectionProps = {\n  projects: Array<Project | null>;\n} & EventsSectionProps;\n\nexport function ProjectsSection({\n  projects,\n  theme,\n  ...rest\n}: ProjectsSectionProps) {\n  return (\n    <EventsSection theme={theme} title=\"Projects\" {...rest}>\n      {projects.map(\n        (project, index) =>\n          project && <ProjectItem key={index} theme={theme} project={project} />\n      )}\n    </EventsSection>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/publications-section.tsx",
    "content": "import { Publication } from \"../../types\";\nimport { Text } from \"@react-pdf/renderer\";\nimport {\n  EventItem,\n  EventsSection,\n  EventsSectionProps,\n} from \"../events-section\";\nimport { ReactElement } from \"react\";\nimport { Theme } from \"../theme\";\n\nexport type PublicationItemProps = {\n  publication: Publication;\n  theme: Theme;\n};\n\nexport function PublicationItem({ publication, theme }: PublicationItemProps) {\n  const titleDetails: Array<ReactElement> = [];\n  const { name, url, publisher, releaseDate, summary } = publication;\n\n  if (publisher) titleDetails.push(<Text>{publisher}</Text>);\n\n  return (\n    <EventItem\n      title={name}\n      description={summary}\n      url={url}\n      titleDetails={titleDetails}\n      date={releaseDate}\n      theme={theme}\n    />\n  );\n}\n\n//\n\ntype PublicationsSectionProps = {\n  publications: Array<Publication | null>;\n} & EventsSectionProps;\n\nexport function PublicationsSection({\n  publications,\n  theme,\n  ...rest\n}: PublicationsSectionProps) {\n  return (\n    <EventsSection theme={theme} title=\"Publications\" {...rest}>\n      {publications.map(\n        (publication, index) =>\n          publication && (\n            <PublicationItem\n              key={index}\n              theme={theme}\n              publication={publication}\n            />\n          )\n      )}\n    </EventsSection>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/skills-section.tsx",
    "content": "import { Skill } from \"../../types\";\nimport {\n  GroupItem,\n  GroupedSection,\n  GroupedSectionProps,\n} from \"../grouped-section\";\n\ntype SkillItemProps = {\n  skill: Skill;\n};\n\nexport function SkillItem({ skill }: SkillItemProps) {\n  const { keywords } = skill;\n  const description =\n    keywords && Array.isArray(keywords) ? keywords.join(\", \") : \"\";\n\n  return <GroupItem title={skill.name} description={description} />;\n}\n\n//\n\ntype SkillsSectionProps = {\n  skills: Array<Skill | null>;\n} & GroupedSectionProps;\n\nexport function SkillsSection({ skills, theme, ...rest }: SkillsSectionProps) {\n  return (\n    <GroupedSection theme={theme} title=\"Skills\" {...rest}>\n      {skills.map(\n        (skill, index) => skill && <SkillItem key={index} skill={skill} />\n      )}\n    </GroupedSection>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/volunteer-section.tsx",
    "content": "import { Voluteering } from \"../../types\";\nimport { Link } from \"@react-pdf/renderer\";\nimport {\n  EventHighlightItem,\n  EventItem,\n  EventsSectionProps,\n  EventsSection,\n} from \"../events-section\";\nimport { ReactElement } from \"react\";\nimport { Theme } from \"../theme\";\n\nexport type VolunteeringItemProps = {\n  volunteering: Voluteering;\n  theme: Theme;\n};\n\nexport function VolunteeringItem({\n  volunteering,\n  theme,\n}: VolunteeringItemProps) {\n  const titleDetails: Array<ReactElement> = [];\n\n  if (volunteering.organization) {\n    titleDetails.push(\n      <Link\n        src={volunteering.url || \"\"}\n        style={{ color: theme.color.text, textDecoration: \"none\" }}\n      >\n        {volunteering.organization}\n      </Link>\n    );\n  }\n\n  return (\n    <EventItem\n      title={volunteering.position}\n      description={volunteering.summary}\n      titleDetails={titleDetails}\n      startDate={volunteering.startDate}\n      endDate={volunteering.endDate}\n      theme={theme}\n    >\n      {volunteering.highlights &&\n        Array.isArray(volunteering.highlights) &&\n        volunteering.highlights.map(\n          (highlight) =>\n            highlight && (\n              <EventHighlightItem key={highlight}>\n                {highlight}\n              </EventHighlightItem>\n            )\n        )}\n    </EventItem>\n  );\n}\n\n//\n\ntype VolunteerSectionProps = {\n  volunteer: Array<Voluteering | null>;\n} & EventsSectionProps;\n\nexport function VolunteerSection({ volunteer, theme }: VolunteerSectionProps) {\n  return (\n    <EventsSection theme={theme} title=\"Volunteer\">\n      {volunteer.map(\n        (volunteering, index) =>\n          volunteering && (\n            <VolunteeringItem\n              key={index}\n              theme={theme}\n              volunteering={volunteering}\n            />\n          )\n      )}\n    </EventsSection>\n  );\n}\n"
  },
  {
    "path": "src/documents/sections/work-section.tsx",
    "content": "import { Job } from \"../../types\";\nimport { Link, Text } from \"@react-pdf/renderer\";\nimport {\n  EventHighlightItem,\n  EventItem,\n  EventsSectionProps,\n  EventsSection,\n} from \"../events-section\";\nimport { ReactElement } from \"react\";\nimport { Theme } from \"../theme\";\n\nexport type JobItemProps = {\n  job: Job;\n  theme: Theme;\n};\n\nexport function JobItem({ job, theme }: JobItemProps) {\n  const titleDetails: Array<ReactElement> = [];\n\n  if (job.name) {\n    titleDetails.push(\n      <Link\n        src={job.url || \"\"}\n        style={{ color: theme.color.text, textDecoration: \"none\" }}\n      >\n        {job.name}\n      </Link>\n    );\n  }\n\n  if (job.location) {\n    titleDetails.push(\n      <Text style={{ color: theme.color.lightText }}>{job.location}</Text>\n    );\n  }\n\n  return (\n    <EventItem\n      title={job.position}\n      description={job.summary}\n      titleDetails={titleDetails}\n      startDate={job.startDate}\n      endDate={job.endDate}\n      theme={theme}\n    >\n      {job.highlights &&\n        Array.isArray(job.highlights) &&\n        job.highlights.map(\n          (hightlight) =>\n            hightlight && (\n              <EventHighlightItem key={hightlight}>\n                {hightlight}\n              </EventHighlightItem>\n            )\n        )}\n    </EventItem>\n  );\n}\n\n//\n\ntype WorkSectionProps = {\n  work: Array<Job | null>;\n} & EventsSectionProps;\n\nexport function WorkSection({ work, theme, ...rest }: WorkSectionProps) {\n  return (\n    <EventsSection theme={theme} title=\"Work Experience\" {...rest}>\n      {work.map(\n        (job, index) => job && <JobItem key={index} theme={theme} job={job} />\n      )}\n    </EventsSection>\n  );\n}\n"
  },
  {
    "path": "src/documents/stack.tsx",
    "content": "import { ReactNode } from \"react\";\nimport { View } from \"@react-pdf/renderer\";\nimport { Style, ViewProps } from \"@react-pdf/types\";\n\ntype Props = ViewProps & {\n  gap?: number;\n  children: ReactNode;\n  flexWrap?: Style[\"flexWrap\"];\n  style?: Style;\n};\n\nexport function HStack({ gap, children, flexWrap, style, ...rest }: Props) {\n  const updatedStyle: Style = {\n    display: \"flex\",\n    flexDirection: \"row\",\n    gap,\n    flexWrap,\n    alignItems: \"center\",\n    ...style,\n  };\n\n  return (\n    <View {...rest} style={updatedStyle}>\n      {children}\n    </View>\n  );\n}\n\nexport function VStack({ gap, children, flexWrap, style, ...rest }: Props) {\n  const updatedStyle: Style = {\n    display: \"flex\",\n    flexDirection: \"column\",\n    gap,\n    flexWrap,\n    ...style,\n  };\n\n  return (\n    <View style={updatedStyle} {...rest}>\n      {children}\n    </View>\n  );\n}\n"
  },
  {
    "path": "src/documents/svg-icon.tsx",
    "content": "import { Svg } from \"@react-pdf/renderer\";\nimport { Style } from \"@react-pdf/types\";\nimport { ReactNode } from \"react\";\n\nexport type SvgIconProps = {\n  size?: number;\n  color?: string;\n  style?: Style;\n  children?: ReactNode;\n};\n\nexport function SvgIcon({ size = 24, style, children }: SvgIconProps) {\n  return (\n    <Svg width={size} height={size} viewBox=\"0 0 24 24\" style={style}>\n      {children}\n    </Svg>\n  );\n}\n"
  },
  {
    "path": "src/documents/theme.ts",
    "content": "export type Theme = {\n  lineHeight: number;\n  space: Array<number>;\n  fontSize: Array<number>;\n  color: {\n    text: string;\n    lightText: string;\n    accent: string;\n  };\n};\n\nexport function createTheme(accentColor = \"#2B5DD6\", baseFontSize = 10) {\n  const clampedBaseFontSize = Math.min(Math.max(10, baseFontSize), 16);\n\n  const result: Theme = {\n    //      0  1  2  3   4   5   6   7   8   9  10  11  12\n    space: [2, 4, 6, 8, 10, 12, 14, 16, 20, 22, 24, 28, 32],\n\n    lineHeight: 1.4,\n\n    fontSize: [\n      clampedBaseFontSize,\n      1.4 * clampedBaseFontSize,\n      1.8 * clampedBaseFontSize,\n    ],\n    color: {\n      text: \"black\",\n      lightText: \"#6b7280\",\n      accent: accentColor,\n    },\n  };\n\n  return result;\n}\n"
  },
  {
    "path": "src/documents/use-has-page-break.ts",
    "content": "import { useCallback, useMemo } from \"react\";\nimport { Meta, ResumeSectionName } from \"../types\";\n\nexport function useHasPageBreak(meta?: Meta | null) {\n  const sectionsPageBreaksSet = useMemo(\n    () =>\n      meta && Array.isArray(meta.sectionsPageBreaks)\n        ? new Set<ResumeSectionName>(meta.sectionsPageBreaks)\n        : null,\n    [meta]\n  );\n\n  const hasPageBreak = useCallback(\n    (sectionName: ResumeSectionName) => {\n      return sectionsPageBreaksSet\n        ? sectionsPageBreaksSet.has(sectionName)\n        : false;\n    },\n    [sectionsPageBreaksSet]\n  );\n\n  return hasPageBreak;\n}\n"
  },
  {
    "path": "src/documents/utils/format-date.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { formatDate } from \".\";\n\nconst tests = [\n  [2021, \"2021\"],\n  [\"2021\", \"2021\"],\n  [\"2021-2\", \"Feb 2021\"],\n  [\"2021-02\", \"Feb 2021\"],\n  [\"2021-02-01\", \"Feb 2021\"],\n  [\"2021-\", \"2021\"],\n  [\"2021-15\", \"Invalid Date 2021\"],\n  [\"2021-0\", \"Invalid Date 2021\"],\n];\n\ntest(\"returns the right format\", () => {\n  for (const [input, expectation] of tests) {\n    expect(formatDate(input)).toEqual(expectation);\n  }\n});\n"
  },
  {
    "path": "src/documents/utils/format-date.ts",
    "content": "export function formatDate(stringOrNumber: string | number): string | null {\n  const dateString = stringOrNumber.toString();\n  const parts = dateString.split(\"-\").filter((part) => part !== \"\");\n  const partsCount = parts.length;\n  if (partsCount === 0) return null;\n\n  const year = parts[0];\n  if (partsCount === 1) return year;\n\n  const date = new Date(dateString);\n  const month = date.toLocaleString(\"en-US\", { month: \"short\" });\n  return `${month} ${year}`;\n}\n"
  },
  {
    "path": "src/documents/utils/get-sections-order.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { defaultSectionsOrder, getSectionsOrder } from \"./get-sections-order\";\nimport { ResumeSectionName } from \"../../types\";\n\ntest(\"returns the default order if no metadata\", () => {\n  const actual = getSectionsOrder();\n  expect(actual).toEqual(defaultSectionsOrder);\n});\n\ntest(\"returns the sections in the specified order \", () => {\n  const sectionsOrder: ResumeSectionName[] = [\n    \"basics\",\n    \"awards\",\n    \"education\",\n    \"volunteer\",\n    \"skills\",\n    \"work\",\n    \"certificates\",\n    \"projects\",\n    \"publications\",\n  ];\n\n  const actual = getSectionsOrder({\n    sectionsOrder,\n  });\n\n  expect(actual).toEqual(sectionsOrder);\n});\n\ntest(\"returns the specified sections in their order and the others in the default one\", () => {\n  const sectionsOrder: ResumeSectionName[] = [\"basics\", \"work\", \"education\"];\n\n  const actual = getSectionsOrder({\n    sectionsOrder,\n  });\n\n  expect(actual).toEqual([\n    ...sectionsOrder,\n    \"skills\",\n    \"projects\",\n    \"awards\",\n    \"certificates\",\n    \"publications\",\n    \"volunteer\",\n  ]);\n});\n"
  },
  {
    "path": "src/documents/utils/get-sections-order.ts",
    "content": "import { Meta, ResumeSectionName } from \"../../types\";\n\nexport const defaultSectionsOrder: ResumeSectionName[] = [\n  \"basics\",\n  \"skills\",\n  \"work\",\n  \"projects\",\n  \"education\",\n  \"awards\",\n  \"certificates\",\n  \"publications\",\n  \"volunteer\",\n];\n\nexport function getSectionsOrder(meta?: Meta | null) {\n  if (!meta || !Array.isArray(meta.sectionsOrder)) {\n    return defaultSectionsOrder;\n  }\n\n  const { sectionsOrder } = meta;\n  const finalSectionsOrder = [...sectionsOrder];\n\n  const leftOutSections = defaultSectionsOrder.filter(\n    (sectionName) => !sectionsOrder.includes(sectionName)\n  );\n  finalSectionsOrder.push(...leftOutSections);\n\n  return finalSectionsOrder;\n}\n"
  },
  {
    "path": "src/documents/utils/index.ts",
    "content": "export * from \"./format-date\";\nexport * from \"./get-sections-order\";\n"
  },
  {
    "path": "src/editing/index.ts",
    "content": "export * from \"./yaml-editor\";\nexport * from \"./schema\";\n"
  },
  {
    "path": "src/editing/schema.css",
    "content": ".Schema {\n  background-color: var(--color-gray-100);\n  border-top: 1px solid var(--color-gray-400);\n  border-right: 1px solid var(--color-gray-400);\n\n  width: 100%;\n  height: 100%;\n  color: var(--color-white);\n  padding: var(--space-6);\n  overflow: scroll;\n}\n\n.Schema-Keyword {\n  color: var(--color-blue-300);\n}\n\n.Schema-Type {\n  color: var(--color-green-100);\n}\n\n.Schema-Plain {\n  color: var(--color-white);\n}\n\n.Schema-Field {\n  color: var(--color-blue-400);\n}\n\n.Schema-Comment {\n  color: var(--color-gray-600);\n}\n"
  },
  {
    "path": "src/editing/schema.tsx",
    "content": "import { createTypeHighlighter } from \"./type-highlighter\";\nimport \"./schema.css\";\nimport { memo, useMemo } from \"react\";\n\nfunction createHighlightedElements() {\n  const highligher = createTypeHighlighter();\n\n  highligher.pushType(\"Resume\");\n\n  highligher.pushObject(\"basics\");\n  highligher.addStringField(\"name\");\n  highligher.addStringField(\"label\");\n  highligher.addStringField(\"email\");\n  highligher.addStringField(\"summary\", { markdown: true });\n  highligher.pushArrayOfObjects(\"profiles\");\n  highligher.addEnumField(\"network\", [\"github\", \"network\"]);\n  highligher.pop();\n  highligher.pop();\n\n  highligher.pushArrayOfObjects(\"work\");\n  highligher.addStringField(\"name\");\n  highligher.addStringField(\"location\");\n  highligher.addStringField(\"position\");\n  highligher.addStringField(\"url\");\n  highligher.addStringField(\"summary\");\n  highligher.addStringField(\"startDate\", { date: true });\n  highligher.addStringField(\"endDate\", { date: true });\n  highligher.addArrayOfStringsField(\"highlights\", { markdown: true });\n  highligher.pop();\n\n  highligher.pushArrayOfObjects(\"skills\");\n  highligher.addStringField(\"name\");\n  highligher.addArrayOfStringsField(\"keywords\");\n  highligher.pop();\n\n  highligher.pushArrayOfObjects(\"work\");\n  highligher.addStringField(\"name\");\n  highligher.addStringField(\"description\", { markdown: true });\n  highligher.addStringField(\"url\");\n  highligher.addStringField(\"startDate\", { date: true });\n  highligher.addStringField(\"endDate\", { date: true });\n  highligher.addArrayOfStringsField(\"highlights\", { markdown: true });\n  highligher.pop();\n\n  highligher.pushArrayOfObjects(\"education\");\n  highligher.addStringField(\"institution\");\n  highligher.addStringField(\"url\");\n  highligher.addStringField(\"area\");\n  highligher.addStringField(\"score\", { markdown: true });\n  highligher.addStringField(\"startDate\", { date: true });\n  highligher.addStringField(\"endDate\", { date: true });\n  highligher.addArrayOfStringsField(\"courses\");\n  highligher.pop();\n\n  highligher.pushArrayOfObjects(\"awards\");\n  highligher.addStringField(\"title\");\n  highligher.addStringField(\"awarder\");\n  highligher.addStringField(\"date\", { date: true });\n  highligher.addStringField(\"summary\", { markdown: true });\n  highligher.pop();\n\n  highligher.pushArrayOfObjects(\"certificates\");\n  highligher.addStringField(\"name\");\n  highligher.addStringField(\"url\");\n  highligher.addStringField(\"date\", { date: true });\n  highligher.addStringField(\"issuer\");\n  highligher.pop();\n\n  highligher.pushArrayOfObjects(\"publications\");\n  highligher.addStringField(\"name\");\n  highligher.addStringField(\"publisher\");\n  highligher.addStringField(\"releaseDate\", { date: true });\n  highligher.addStringField(\"url\");\n  highligher.addStringField(\"summary\", { markdown: true });\n  highligher.pop();\n\n  highligher.pushArrayOfObjects(\"volunteer\");\n  highligher.addStringField(\"organization\");\n  highligher.addStringField(\"position\");\n  highligher.addStringField(\"url\");\n  highligher.addStringField(\"summary\", { markdown: true });\n  highligher.addStringField(\"startDate\", { date: true });\n  highligher.addStringField(\"endDate\", { date: true });\n  highligher.addArrayOfStringsField(\"highlights\", { markdown: true });\n  highligher.pop();\n\n  highligher.pushObject(\"meta\");\n  highligher.addStringField(\"accentColor\");\n  highligher.addNumberField(\"baseFontSize\");\n  highligher.addArrayOfStringsField(\"sectionsOrder\", {\n    comment: `how are sections ordered, e.g., [\"basics\", \"work\", \"education\", ...]`,\n  });\n  highligher.addArrayOfStringsField(\"sectionsPageBreaks\", {\n    comment: `which sections should start on a new page, e.g., [\"basics\", \"work\", \"education\", ...]`,\n  });\n  highligher.pop();\n\n  highligher.pop();\n\n  return highligher.result;\n}\n\nexport const Schema = memo(function () {\n  const highlightedElements = useMemo(createHighlightedElements, []);\n\n  return (\n    <div className=\"Schema\">\n      <pre\n        style={{\n          margin: 0,\n          lineHeight: 1.5,\n          fontFamily: \"Monaco, Courier, monospace\",\n          fontSize: 14,\n        }}\n      >\n        <span className=\"Schema-Comment\">\n          // Use this as a guide. It shows the supported fields from the JSON\n          resume schema\n          <br />\n          // (jsonresume.org/schema) expressed in TypeScript so they're easier\n          to read. <br />\n          // -------------------------------------------------------------------\n          <br />\n          // Where noted you can use the following Markdown subset: <br />\n          // *bold*, **italics**, [label](link). <br />\n        </span>\n        <br />\n\n        {highlightedElements}\n      </pre>\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/editing/type-highlighter.tsx",
    "content": "import { Fragment, ReactElement } from \"react\";\n\n// eslint-disable-next-line react-refresh/only-export-components\nconst IDENT = \"  \";\n\nexport function createTypeHighlighter() {\n  const result: ReactElement[] = [];\n  let space = \"\";\n  let lastKey = 0;\n\n  function pushType(name: string) {\n    result.push(\n      <Fragment key={lastKey++}>\n        <span className=\"Schema-Keyword\">type</span>{\" \"}\n        <span className=\"Schema-Type\">{name}</span>{\" \"}\n        <span className=\"Schema-Plan\">=</span>{\" \"}\n        <span className=\"Schema-Plain\">{\"{\"}</span>\n        <br />\n      </Fragment>\n    );\n\n    space += IDENT;\n  }\n\n  function pushObject(name: string) {\n    result.push(\n      <Fragment key={lastKey++}>\n        <span className=\"Schema-Field\">\n          {space}\n          {name}\n        </span>\n        ?: <span className=\"Schema-Plain\">{\"{\"}</span>\n        <br />\n      </Fragment>\n    );\n\n    space += IDENT;\n  }\n\n  function pushArrayOfObjects(name: string) {\n    result.push(\n      <Fragment key={lastKey++}>\n        <span className=\"Schema-Field\">\n          {space}\n          {name}\n        </span>\n        ?: <span className=\"Schema-Type\">Array</span>\n        <span className=\"Schema-Plain\">&lt;</span>\n        <span className=\"Schema-Plain\">{\"{\"}</span>\n        <br />\n      </Fragment>\n    );\n\n    space += IDENT;\n  }\n\n  function pop() {\n    space = space.slice(0, space.length - IDENT.length);\n\n    result.push(\n      <Fragment key={lastKey++}>\n        <span className=\"Schema-Plain\">\n          {space}\n          {\"}\"}\n        </span>\n        <span className=\"Schema-Plain\">;</span>\n        <br />\n      </Fragment>\n    );\n  }\n\n  function renderFieldName(name: string) {\n    return (\n      <span className=\"Schema-Field\">\n        {space}\n        {name}\n      </span>\n    );\n  }\n\n  function renderFieldEnd({\n    markdown = false,\n    date = false,\n    comment,\n  }: { markdown?: boolean; date?: boolean; comment?: string } = {}) {\n    return (\n      <>\n        <span className=\"Schema-Plain\">;</span>\n        {markdown && (\n          <span className=\"Schema-Comment\"> // supports Markdown subset</span>\n        )}\n        {date && <span className=\"Schema-Comment\"> // YYYY or YYYY-MM</span>}\n        {comment && <span className=\"Schema-Comment\"> // {comment}</span>}\n        <br />\n      </>\n    );\n  }\n\n  function addStringField(\n    name: string,\n    {\n      markdown = false,\n      date = false,\n    }: { markdown?: boolean; date?: boolean } = {}\n  ) {\n    result.push(\n      <Fragment key={lastKey++}>\n        {renderFieldName(name)}\n        ?: <span className=\"Schema-Type\">string</span>\n        {renderFieldEnd({ markdown, date })}\n      </Fragment>\n    );\n  }\n\n  function addNumberField(name: string) {\n    result.push(\n      <Fragment key={lastKey++}>\n        {renderFieldName(name)}\n        ?: <span className=\"Schema-Type\">number</span>\n        {renderFieldEnd()}\n      </Fragment>\n    );\n  }\n\n  function addEnumField(name: string, values: Array<string>) {\n    result.push(\n      <Fragment key={lastKey++}>\n        {renderFieldName(name)}\n        ?:{\" \"}\n        <span className=\"pl-s\">\n          {values.map((value) => `\"${value}\"`).join(\" | \")}\n        </span>\n        {renderFieldEnd()}\n      </Fragment>\n    );\n  }\n\n  function addArrayOfStringsField(\n    name: string,\n    { markdown = false, comment }: { markdown?: boolean; comment?: string } = {}\n  ) {\n    result.push(\n      <Fragment key={lastKey++}>\n        {renderFieldName(name)}\n        ?: <span className=\"Schema-Type\">Array</span>\n        <span className=\"Schema-Plain\">&lt;</span>\n        <span className=\"Schema-Type\">string</span>\n        <span className=\"Schema-Plain\">&gt;</span>\n        {renderFieldEnd({ markdown, comment })}\n      </Fragment>\n    );\n  }\n\n  return {\n    pushObject,\n    pushArrayOfObjects,\n    addStringField,\n    addArrayOfStringsField,\n    pop,\n    pushType,\n    addEnumField,\n\n    addNumberField,\n    result,\n  };\n}\n"
  },
  {
    "path": "src/editing/yaml-editor.css",
    "content": ".YAMLEditor {\n  font-size: var(--font-size-1);\n  overflow-y: hidden;\n  height: 100%;\n  border-right: 1px solid var(--color-gray-400);\n}\n"
  },
  {
    "path": "src/editing/yaml-editor.tsx",
    "content": "import CodeMirror, { ReactCodeMirrorRef } from \"@uiw/react-codemirror\";\nimport * as yamlMode from \"@codemirror/legacy-modes/mode/yaml\";\nimport { StreamLanguage } from \"@codemirror/language\";\nimport { vscodeDarkInit } from \"@uiw/codemirror-theme-vscode\";\nimport { RefObject, memo } from \"react\";\nimport \"./yaml-editor.css\";\n\nconst yaml = StreamLanguage.define(yamlMode.yaml);\n\ntype Props = {\n  value: string;\n  onChange: (value: string) => void;\n  codeMirrorRef: RefObject<ReactCodeMirrorRef>;\n};\n\nexport const YAMLEditor = memo(function ({\n  value,\n  onChange,\n  codeMirrorRef,\n}: Props) {\n  return (\n    <CodeMirror\n      ref={codeMirrorRef}\n      className=\"YAMLEditor\"\n      value={value}\n      onChange={onChange}\n      theme={vscodeDarkInit({\n        settings: { fontFamily: \"Monaco, Courier, monospace\" },\n      })}\n      extensions={[yaml]}\n    />\n  );\n});\n"
  },
  {
    "path": "src/icons/download-icon.tsx",
    "content": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function DownloadIcon({ size = 24, style }: Props) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      style={style}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"feather feather-download\"\n    >\n      <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path>\n      <polyline points=\"7 10 12 15 17 10\"></polyline>\n      <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/icons/folder-icon.tsx",
    "content": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function FolderIcon({ size = 24, style }: Props) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      style={style}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"feather feather-folder\"\n    >\n      <path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\"></path>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/icons/index.ts",
    "content": "export * from \"./folder-icon\";\nexport * from \"./download-icon\";\nexport * from \"./pdf-icon\";\nexport * from \"./zoom-in-icon\";\nexport * from \"./zoom-out-icon\";\nexport * from \"./info-icon\";\nexport * from \"./plus-icon\";\n"
  },
  {
    "path": "src/icons/info-icon.tsx",
    "content": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function InfoIcon({ size, style }: Props) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      style={style}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"feather feather-info\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\n      <line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"></line>\n      <line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"></line>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/icons/pdf-icon.tsx",
    "content": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function PDFIcon({ size, style }: Props) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      style={style}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"feather feather-file-text\"\n    >\n      <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"></path>\n      <polyline points=\"14 2 14 8 20 8\"></polyline>\n      <line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"></line>\n      <line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"></line>\n      <polyline points=\"10 9 9 9 8 9\"></polyline>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/icons/plus-icon.tsx",
    "content": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function PlusIcon({ size, style }: Props) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      style={style}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"feather feather-plus\"\n    >\n      <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"></line>\n      <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/icons/zoom-in-icon.tsx",
    "content": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function ZoomInIcon({ size, style }: Props) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      style={style}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"feather feather-zoom-in\"\n    >\n      <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n      <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n      <line x1=\"11\" y1=\"8\" x2=\"11\" y2=\"14\"></line>\n      <line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\"></line>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/icons/zoom-out-icon.tsx",
    "content": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function ZoomOutIcon({ size, style }: Props) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      style={style}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"feather feather-zoom-out\"\n    >\n      <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n      <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n      <line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\"></line>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/index.css",
    "content": "body {\n  margin: 0;\n}\nbody,\nbutton,\ninput {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n    Oxygen, Ubuntu, Cantarell, \"Open Sans\", \"Helvetica Neue\", sans-serif;\n}\n\n:root {\n  --color-gray-100: #1e1e1e;\n  --color-gray-200: #2a2a2a;\n  --color-gray-300: #363636;\n  --color-gray-400: #454545;\n  --color-gray-500: #555555;\n  --color-gray-600: #777777;\n\n  --color-blue-100: #2b5dd6;\n  --color-blue-200: #3b82f6;\n  --color-blue-300: #569cd6;\n  --color-blue-400: #9cdcfe;\n\n  --color-green-100: #4ec9b0;\n\n  --color-white: #ffffff;\n  --color-black: #000000;\n\n  --space-1: 0.125rem;\n  --space-2: 0.375rem;\n  --space-3: 0.5rem;\n  --space-4: 0.625rem;\n  --space-5: 0.875rem;\n  --space-6: 1rem;\n  --space-7: 1.25rem;\n  --space-8: 1.375rem;\n\n  --font-size-1: 0.875rem;\n  --font-size-2: 1rem;\n  --font-size-3: 1.25rem;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n.cm-editor {\n  height: 100%;\n}\n\n::-webkit-scrollbar {\n  width: var(--space-4);\n  height: var(--space-4);\n}\n\n::-webkit-scrollbar-thumb {\n  background: var(--color-gray-500);\n}\n\n::-webkit-scrollbar-thumb:hover {\n  background: var(--color-gray-600);\n}\n::-webkit-scrollbar-thumb:active {\n  background: var(--color-white);\n}\n::-webkit-scrollbar-corner {\n  background: var(--color-gray-100);\n}\n\nbutton,\na {\n  border: none;\n  padding: var(--space-2) var(--space-8) var(--space-2);\n  background-color: var(--color-gray-300);\n  color: var(--color-white);\n  cursor: pointer;\n  font-size: var(--font-size-1);\n}\n\nbutton:hover,\na:hover {\n  background-color: var(--color-gray-500);\n}\n\nbutton.primary {\n  background-color: var(--color-blue-100);\n}\n\nbutton.primary:hover {\n  background-color: var(--color-blue-200);\n}\n\nbutton:active,\nbutton.primary:active,\na:active {\n  color: var(--color-black);\n  background-color: var(--color-white);\n}\n\nbutton:disabled,\na:disabled {\n  opacity: 0.5;\n  pointer-events: none;\n}\n"
  },
  {
    "path": "src/main.tsx",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { App } from \"./App\";\nimport \"./index.css\";\nimport { pdfjs } from \"react-pdf\";\n\npdfjs.GlobalWorkerOptions.workerSrc = new URL(\n  \"pdfjs-dist/build/pdf.worker.min.js\",\n  import.meta.url\n).toString();\n\nconst registerServiceWorker = async () => {\n  if (\"serviceWorker\" in navigator) {\n    try {\n      const registration = await navigator.serviceWorker.register(\n        \"/cache-sw.js\",\n        {\n          scope: \"/\",\n        }\n      );\n\n      if (registration.active && !navigator.serviceWorker.controller) {\n        window.location.reload();\n      }\n    } catch (error) {\n      console.error(`Registration failed with ${error}`);\n    }\n  }\n};\n\nregisterServiceWorker();\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n"
  },
  {
    "path": "src/panes-layout.tsx",
    "content": "import { ReactElement, useState } from \"react\";\nimport { Pane, SashContent } from \"split-pane-react\";\nimport SplitPane from \"split-pane-react/esm/SplitPane\";\n\ntype Props = {\n  left: ReactElement;\n  bottom: ReactElement;\n  right: ReactElement;\n};\n\nfunction sashRender(_: number, active: boolean) {\n  return <SashContent active={active} type=\"vscode\" />;\n}\n\nexport function PanesLayout({ left, bottom, right }: Props) {\n  const [horizontalSizes, setHorizontalSizes] = useState<\n    Array<string | number>\n  >([\"80%\"]);\n\n  const [verticalSizes, setVerticalSizes] = useState<Array<string | number>>([\n    \"50%\",\n  ]);\n\n  return (\n    <SplitPane\n      sashRender={sashRender}\n      split=\"vertical\"\n      sizes={verticalSizes}\n      onChange={setVerticalSizes}\n    >\n      <Pane minSize=\"30%\" maxSize=\"50%\">\n        <SplitPane\n          sashRender={sashRender}\n          split=\"horizontal\"\n          sizes={horizontalSizes}\n          onChange={setHorizontalSizes}\n        >\n          {left}\n          <Pane minSize=\"5%\">{bottom}</Pane>\n        </SplitPane>\n      </Pane>\n\n      {right}\n    </SplitPane>\n  );\n}\n"
  },
  {
    "path": "src/parsing/index.ts",
    "content": "export * from \"./use-yaml-parsing\";\n"
  },
  {
    "path": "src/parsing/parse-yaml.ts",
    "content": "import YAML from \"yaml\";\n\ntype ParseResult = {\n  json?: object;\n  errorMessage?: string;\n};\n\nexport function parseYAML(yaml: string): ParseResult {\n  try {\n    const json = YAML.parse(yaml, { prettyErrors: true });\n    return { json };\n  } catch (error) {\n    return { errorMessage: (error as Error).message };\n  }\n}\n"
  },
  {
    "path": "src/parsing/resume-schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"additionalProperties\": false,\n  \"definitions\": {\n    \"iso8601\": {\n      \"type\": \"string\",\n      \"description\": \"Similar to the standard date type, but each section after the year is optional. e.g. 2014-06-29 or 2023-04\",\n      \"pattern\": \"^([1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]|[1-2][0-9]{3}-[0-1][0-9]|[1-2][0-9]{3})$\"\n    }\n  },\n  \"properties\": {\n    \"$schema\": {\n      \"type\": \"string\",\n      \"description\": \"link to the version of the schema that can validate the resume\",\n      \"format\": \"uri\"\n    },\n    \"basics\": {\n      \"type\": \"object\",\n      \"additionalProperties\": true,\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"label\": {\n          \"type\": \"string\",\n          \"description\": \"e.g. Web Developer\"\n        },\n        \"image\": {\n          \"type\": \"string\",\n          \"description\": \"URL (as per RFC 3986) to a image in JPEG or PNG format\"\n        },\n        \"email\": {\n          \"type\": \"string\",\n          \"description\": \"e.g. thomas@gmail.com\",\n          \"format\": \"email\"\n        },\n        \"phone\": {\n          \"type\": \"string\",\n          \"description\": \"Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923\"\n        },\n        \"url\": {\n          \"type\": \"string\",\n          \"description\": \"URL (as per RFC 3986) to your website, e.g. personal homepage\",\n          \"format\": \"uri\"\n        },\n        \"summary\": {\n          \"type\": \"string\",\n          \"description\": \"Write a short 2-3 sentence biography about yourself\"\n        },\n        \"location\": {\n          \"type\": \"object\",\n          \"additionalProperties\": true,\n          \"properties\": {\n            \"address\": {\n              \"type\": \"string\",\n              \"description\": \"To add multiple address lines, use \\n. For example, 1234 Glücklichkeit Straße\\nHinterhaus 5. Etage li.\"\n            },\n            \"postalCode\": {\n              \"type\": \"string\"\n            },\n            \"city\": {\n              \"type\": \"string\"\n            },\n            \"countryCode\": {\n              \"type\": \"string\",\n              \"description\": \"code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN\"\n            },\n            \"region\": {\n              \"type\": \"string\",\n              \"description\": \"The general region where you live. Can be a US state, or a province, for instance.\"\n            }\n          }\n        },\n        \"profiles\": {\n          \"type\": \"array\",\n          \"description\": \"Specify any number of social networks that you participate in\",\n          \"additionalItems\": false,\n          \"items\": {\n            \"type\": \"object\",\n            \"additionalProperties\": true,\n            \"properties\": {\n              \"network\": {\n                \"type\": \"string\",\n                \"description\": \"e.g. Facebook or Twitter\"\n              },\n              \"username\": {\n                \"type\": \"string\",\n                \"description\": \"e.g. neutralthoughts\"\n              },\n              \"url\": {\n                \"type\": \"string\",\n                \"description\": \"e.g. http://twitter.example.com/neutralthoughts\",\n                \"format\": \"uri\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"work\": {\n      \"type\": \"array\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Facebook\"\n          },\n          \"location\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Menlo Park, CA\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Social Media Company\"\n          },\n          \"position\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Software Engineer\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. http://facebook.example.com\",\n            \"format\": \"uri\"\n          },\n          \"startDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"endDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"summary\": {\n            \"type\": \"string\",\n            \"description\": \"Give an overview of your responsibilities at the company\"\n          },\n          \"highlights\": {\n            \"type\": \"array\",\n            \"description\": \"Specify multiple accomplishments\",\n            \"additionalItems\": false,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"e.g. Increased profits by 20% from 2011-2012 through viral advertising\"\n            }\n          }\n        }\n      }\n    },\n    \"volunteer\": {\n      \"type\": \"array\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"organization\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Facebook\"\n          },\n          \"position\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Software Engineer\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. http://facebook.example.com\",\n            \"format\": \"uri\"\n          },\n          \"startDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"endDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"summary\": {\n            \"type\": \"string\",\n            \"description\": \"Give an overview of your responsibilities at the company\"\n          },\n          \"highlights\": {\n            \"type\": \"array\",\n            \"description\": \"Specify accomplishments and achievements\",\n            \"additionalItems\": false,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"e.g. Increased profits by 20% from 2011-2012 through viral advertising\"\n            }\n          }\n        }\n      }\n    },\n    \"education\": {\n      \"type\": \"array\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"institution\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Massachusetts Institute of Technology\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. http://facebook.example.com\",\n            \"format\": \"uri\"\n          },\n          \"area\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Arts\"\n          },\n          \"studyType\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Bachelor\"\n          },\n          \"startDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"endDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"score\": {\n            \"type\": \"string\",\n            \"description\": \"grade point average, e.g. 3.67/4.0\"\n          },\n          \"courses\": {\n            \"type\": \"array\",\n            \"description\": \"List notable courses/subjects\",\n            \"additionalItems\": false,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"e.g. H1302 - Introduction to American history\"\n            }\n          }\n        }\n      }\n    },\n    \"awards\": {\n      \"type\": \"array\",\n      \"description\": \"Specify any awards you have received throughout your professional career\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"title\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. One of the 100 greatest minds of the century\"\n          },\n          \"date\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"awarder\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Time Magazine\"\n          },\n          \"summary\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Received for my work with Quantum Physics\"\n          }\n        }\n      }\n    },\n    \"certificates\": {\n      \"type\": \"array\",\n      \"description\": \"Specify any certificates you have received throughout your professional career\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Certified Kubernetes Administrator\"\n          },\n          \"date\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. http://example.com\",\n            \"format\": \"uri\"\n          },\n          \"issuer\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. CNCF\"\n          }\n        }\n      }\n    },\n    \"publications\": {\n      \"type\": \"array\",\n      \"description\": \"Specify your publications through your career\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. The World Wide Web\"\n          },\n          \"publisher\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. IEEE, Computer Magazine\"\n          },\n          \"releaseDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. http://www.computer.org.example.com/csdl/mags/co/1996/10/rx069-abs.html\",\n            \"format\": \"uri\"\n          },\n          \"summary\": {\n            \"type\": \"string\",\n            \"description\": \"Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML.\"\n          }\n        }\n      }\n    },\n    \"skills\": {\n      \"type\": \"array\",\n      \"description\": \"List out your professional skill-set\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Web Development\"\n          },\n          \"level\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Master\"\n          },\n          \"keywords\": {\n            \"type\": \"array\",\n            \"description\": \"List some keywords pertaining to this skill\",\n            \"additionalItems\": false,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"e.g. HTML\"\n            }\n          }\n        }\n      }\n    },\n    \"languages\": {\n      \"type\": \"array\",\n      \"description\": \"List any other languages you speak\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"language\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. English, Spanish\"\n          },\n          \"fluency\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Fluent, Beginner\"\n          }\n        }\n      }\n    },\n    \"interests\": {\n      \"type\": \"array\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Philosophy\"\n          },\n          \"keywords\": {\n            \"type\": \"array\",\n            \"additionalItems\": false,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"e.g. Friedrich Nietzsche\"\n            }\n          }\n        }\n      }\n    },\n    \"references\": {\n      \"type\": \"array\",\n      \"description\": \"List references you have received\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Timothy Cook\"\n          },\n          \"reference\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing.\"\n          }\n        }\n      }\n    },\n    \"projects\": {\n      \"type\": \"array\",\n      \"description\": \"Specify career projects\",\n      \"additionalItems\": false,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": true,\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"e.g. The World Wide Web\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"description\": \"Short summary of project. e.g. Collated works of 2017.\"\n          },\n          \"highlights\": {\n            \"type\": \"array\",\n            \"description\": \"Specify multiple features\",\n            \"additionalItems\": false,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"e.g. Directs you close but not quite there\"\n            }\n          },\n          \"keywords\": {\n            \"type\": \"array\",\n            \"description\": \"Specify special elements involved\",\n            \"additionalItems\": false,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"e.g. AngularJS\"\n            }\n          },\n          \"startDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"endDate\": {\n            \"$ref\": \"#/definitions/iso8601\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"format\": \"uri\",\n            \"description\": \"e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html\"\n          },\n          \"roles\": {\n            \"type\": \"array\",\n            \"description\": \"Specify your role on this project or in company\",\n            \"additionalItems\": false,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"e.g. Team Lead, Speaker, Writer\"\n            }\n          },\n          \"entity\": {\n            \"type\": \"string\",\n            \"description\": \"Specify the relevant company/entity affiliations e.g. 'greenpeace', 'corporationXYZ'\"\n          },\n          \"type\": {\n            \"type\": \"string\",\n            \"description\": \" e.g. 'volunteering', 'presentation', 'talk', 'application', 'conference'\"\n          }\n        }\n      }\n    },\n    \"meta\": {\n      \"type\": \"object\",\n      \"description\": \"The schema version and any other tooling configuration lives here\",\n      \"additionalProperties\": true,\n      \"properties\": {\n        \"canonical\": {\n          \"type\": \"string\",\n          \"description\": \"URL (as per RFC 3986) to latest version of this document\",\n          \"format\": \"uri\"\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"A version field which follows semver - e.g. v1.0.0\"\n        },\n        \"lastModified\": {\n          \"type\": \"string\",\n          \"description\": \"Using ISO 8601 with YYYY-MM-DDThh:mm:ss\"\n        }\n      }\n    }\n  },\n  \"title\": \"Resume Schema\",\n  \"type\": \"object\"\n}\n"
  },
  {
    "path": "src/parsing/sample.ts",
    "content": "export const SAMPLE_YAML = `\n# Demo resume adapted from https://github.com/jsonresume/resume-schema/blob/master/sample.resume.json\n\nbasics:\n  name: Richard Hendriks\n  label: Programmer\n  email: richard.hendriks@mail.com\n  phone: (912) 555-4321\n  url: http://richardhendricks.example.com\n  summary: \n    Richard hails from Tulsa. He has earned degrees from the \n    University of Oklahoma and Stanford. (Go Sooners and Cardinal!) \n    Before starting Pied Piper, he worked for Hooli as a part time \n    software developer. While his work focuses on applied information theory, \n    mostly optimizing lossless compression schema of both the length-limited \n    and adaptive variants, his non-work interests range widely, everything \n    from quantum computing to chaos theory. He could tell you about it, but \n    THAT would NOT be a “length-limited” conversation!\n  location:\n    city: San Francisco\n    countryCode: US\n  profiles:\n    - network: github\n      url: github.com/richardhendricks\n    - network: linkedin\n      url: linkedin.com/richardhendricks\nwork:\n  - name: Pied Piper\n    position: CEO/President\n    location: Palo Alto, CA\n    url: http://piedpiper.example.com\n    summary: \n      Pied Piper is a multi-platform technology based on a proprietary \n      universal compression algorithm that has consistently fielded high\n      Weisman Scores™ that are not merely competitive, but approach the \n      theoretical limit of lossless compression.\n    startDate: 2013-12\n    endDate: 2014-12\n    highlights: \n      - Build an algorithm for artist to detect if their music was violating \n        copy right infringement laws\n        \n      - Successfully won Techcrunch Disrupt\n\n      - Optimized an algorithm that holds the current world record for \n        Weisman Scores\n         \nvolunteer:\n  - organization: CoderDojo\n    position: Teacher\n    url: http://coderdojo.example.com/\n    startDate: 2012-01\n    endDate: 2013-01\n    highlights:\n      - Awarded 'Teacher of the Month'\n\neducation:\n  - institution: University of Oklahoma\n    url: https://www.ou.edu/\n    area: Information Technology\n    score: 'Score: 4.0'\n    startDate: 2011-06\n    endDate: 2014-01\n    courses:\n      - DB1101 - Basic SQL\n      - CS2011 - Java Introduction\n\nawards:\n  - title: Digital Compression Pioneer Award\n    date: 2014-11\n    awarder: Techcrunch\n    summary: There is no spoon.\n\npublications:\n  - name: Video compression for 3d media\n    publisher: Hooli\n    releaseDate: 2014-10\n    url: http://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)\n    summary: \n      Innovative middle-out compression algorithm that changes \n      the way we store data. \n\nskills:\n  - name: Web Development\n    keywords: [\"HTML\", \"CSS\", \"Javascript\"]\n    \n  - name: Compression\n    keywords: [\"Mpeg\", \"MP4\", \"GIF\"]\n\nprojects: \n  - name: Miss Direction\n    description: A mapping engine that misguides you\n    startDate: 2016-08\n    endDate: 2016-08\n    url: missdirection.example.com\n    highlights:\n      - Won award at AIHacks 2016\n      \n      - Built by all women team of newbie programmers\n      \n      - Using modern technologies such as GoogleMaps, Chrome Extension \n        and Javascript\n`;\n"
  },
  {
    "path": "src/parsing/use-yaml-parsing.test.tsx",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { act, renderHook } from \"@testing-library/react\";\nimport { useYAMLParsing } from \".\";\nimport { SAMPLE_YAML } from \"./sample\";\nimport { createDeferred } from \"../utils\";\n\nconst yaml = `\nbasics:\n  name: Test\n`;\n\nconst invalidYAML = `\nbasics\n  name:\n    test\n`;\n\nconst json = { basics: { name: \"Test\" } };\n\n//\n\ntest(\"returns the sample YAML if never used before\", () => {\n  const { result } = renderHook(() =>\n    useYAMLParsing({ onYAMLParsed: () => {} })\n  );\n  expect(result.current.yaml).toEqual(SAMPLE_YAML);\n});\n\ntest(\"updates the value in localStorage\", () => {\n  const yamlDeferred = createDeferred<string>();\n\n  const { result } = renderHook(() =>\n    useYAMLParsing({\n      onYAMLParsed: (yaml) => {\n        yamlDeferred.resolve(yaml);\n      },\n    })\n  );\n\n  act(() => {\n    result.current.setYAML(yaml);\n  });\n\n  expect(yamlDeferred.promise).resolves.toBe(yaml);\n});\n\ndescribe(\"callback\", () => {\n  test(\"calls with the YAML and the parsed json if valid\", async () => {\n    const yamlDeferred = createDeferred<string>();\n    const jsonDeferred = createDeferred<object | undefined>();\n\n    const { result } = renderHook(() =>\n      useYAMLParsing({\n        onYAMLParsed: (yaml, json) => {\n          yamlDeferred.resolve(yaml);\n          jsonDeferred.resolve(json);\n        },\n      })\n    );\n\n    act(() => {\n      result.current.setYAML(yaml);\n    });\n\n    expect(yamlDeferred.promise).resolves.toBe(yaml);\n    expect(jsonDeferred.promise).resolves.toStrictEqual(json);\n  });\n\n  test(\"calls with the YAML and null if invalid\", async () => {\n    const yamlDeferred = createDeferred<string>();\n    const jsonDeferred = createDeferred<object | undefined>();\n\n    const { result } = renderHook(() =>\n      useYAMLParsing({\n        onYAMLParsed: (yaml, json) => {\n          yamlDeferred.resolve(yaml);\n          jsonDeferred.resolve(json);\n        },\n      })\n    );\n\n    act(() => {\n      result.current.setYAML(invalidYAML);\n    });\n\n    expect(yamlDeferred.promise).resolves.toBe(invalidYAML);\n    expect(jsonDeferred.promise).resolves.toBe(undefined);\n  });\n});\n"
  },
  {
    "path": "src/parsing/use-yaml-parsing.ts",
    "content": "import { useCallback, useState } from \"react\";\nimport { useDebouncedEffect } from \"../utils\";\nimport { yamlToJSON } from \"./yaml-to-json\";\nimport { SAMPLE_YAML } from \"./sample\";\n\ntype Props = {\n  onYAMLParsed: (yaml: string, json: object | undefined) => void;\n};\n\nexport const STORAGE_KEY = \"yaml\";\n\nexport function useYAMLParsing({ onYAMLParsed }: Props) {\n  const [yaml, setYAML] = useState(() => {\n    const stored = localStorage.getItem(STORAGE_KEY);\n\n    if (!stored && stored !== \"\") {\n      return SAMPLE_YAML;\n    }\n\n    return stored || \"\";\n  });\n\n  const onCodeUpdate = useCallback(() => {\n    localStorage.setItem(STORAGE_KEY, yaml);\n    const { json, errors } = yamlToJSON(yaml);\n\n    if (process.env.NODE_ENV !== \"test\") {\n      if (json) console.log(\"JSON:\", json);\n      if (errors) console.error(\"Errors:\", errors);\n    }\n\n    onYAMLParsed(yaml, json);\n  }, [yaml, onYAMLParsed]);\n\n  useDebouncedEffect(onCodeUpdate);\n\n  return {\n    setYAML,\n    yaml,\n  };\n}\n"
  },
  {
    "path": "src/parsing/validate-json.ts",
    "content": "// Validaiton\n\nimport Validator from \"z-schema\";\nimport resumeSchema from \"./resume-schema.json\";\n\nconst validator = new Validator({});\n\ntype ValidationResult = {\n  errorMessages?: string[];\n};\n\nexport function validateJSON(json: object): ValidationResult {\n  validator.validate(json, resumeSchema);\n  const errors = validator.getLastErrors();\n\n  if (errors) {\n    const errorMessages = errors.map(\n      (error) => `${error.path}: ${error.message}`\n    );\n    return { errorMessages };\n  }\n\n  return {};\n}\n"
  },
  {
    "path": "src/parsing/yaml-to-json.ts",
    "content": "import { parseYAML } from \"./parse-yaml\";\nimport { validateJSON } from \"./validate-json\";\n\ntype Result = {\n  json?: object;\n  errors?: {\n    type: \"parsing\" | \"validation\";\n    messages: string[];\n  };\n};\n\nfunction resultForParseError(errorMessage: string): Result {\n  return {\n    errors: {\n      type: \"parsing\",\n      messages: [errorMessage],\n    },\n  };\n}\n\nfunction resultFromValidationErrors(errorsMessages: string[]): Result {\n  return {\n    errors: {\n      type: \"validation\",\n      messages: errorsMessages,\n    },\n  };\n}\n\nexport function yamlToJSON(yaml: string): Result {\n  const parseResult = parseYAML(yaml);\n\n  if (parseResult.errorMessage)\n    return resultForParseError(parseResult.errorMessage);\n\n  if (parseResult.json && typeof parseResult.json === \"object\") {\n    const validationResult = validateJSON(parseResult.json);\n\n    if (validationResult.errorMessages)\n      return resultFromValidationErrors(validationResult.errorMessages);\n\n    return {\n      json: parseResult.json,\n    };\n  }\n\n  return {};\n}\n"
  },
  {
    "path": "src/persistence/file-management.ts",
    "content": "export function downloadFile(fileName: string, blob: Blob) {\n  const a: HTMLAnchorElement = document.createElement(\"a\");\n  a.style.display = \"none\";\n  document.body.appendChild(a);\n\n  const url = window.URL.createObjectURL(blob);\n  a.href = url;\n  a.download = fileName;\n  a.click();\n  a.remove();\n  window.URL.revokeObjectURL(url);\n}\n\nexport function readFile(file: File) {\n  return new Promise<string>((resolve, reject) => {\n    const fileReader = new FileReader();\n\n    fileReader.onload = () => {\n      const { result } = fileReader;\n      resolve(result as string);\n    };\n\n    fileReader.onerror = () => {\n      const { error } = fileReader;\n      fileReader.abort();\n      reject(error);\n    };\n\n    fileReader.readAsText(file);\n  });\n}\n\nfunction createFileInput(accept: string) {\n  const input = document.createElement(\"input\");\n  input.type = \"file\";\n  input.multiple = false;\n  input.accept = accept;\n\n  return input;\n}\n\nexport function selectFile(accept: string) {\n  return new Promise<File>((resolve) => {\n    const input = createFileInput(accept);\n\n    input.addEventListener(\"change\", function onChange(this: HTMLInputElement) {\n      const { files } = this;\n\n      if (files && files.length > 0) {\n        resolve(files[0]);\n      }\n    });\n    input.dispatchEvent(new MouseEvent(\"click\"));\n  });\n}\n"
  },
  {
    "path": "src/persistence/index.ts",
    "content": "export * from \"./use-yaml-persistence\";\nexport * from \"./file-management\";\n"
  },
  {
    "path": "src/persistence/use-yaml-persistence.test.ts",
    "content": "import { test, expect, describe, afterEach } from \"vitest\";\nimport { renderHook } from \"@testing-library/react\";\nimport { useYAMLPersistence } from \".\";\nimport * as fileManagement from \"./file-management\";\nimport { vi } from \"vitest\";\nimport { createDeferred } from \"../utils\";\n\ndescribe(\"use-yaml-persistence\", () => {\n  afterEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  test.only(\"saves the file with the right title\", () => {\n    const downloadFileSpy = vi\n      .spyOn(fileManagement, \"downloadFile\")\n      .mockImplementation(() => undefined);\n\n    const { result } = renderHook(() =>\n      useYAMLPersistence({ title: \"title\", yaml: \"\", onFileOpened: () => {} })\n    );\n    result.current.save();\n\n    expect(downloadFileSpy.mock.calls[0][0]).toBe(\"title.yaml\");\n  });\n\n  test(\"saves the file with the right title\", () => {\n    const fileName = \"file.yaml\";\n    const fileContents = \"contents\";\n\n    vi.spyOn(fileManagement, \"selectFile\").mockImplementation(() =>\n      Promise.resolve({ name: fileName } as File)\n    );\n\n    vi.spyOn(fileManagement, \"readFile\").mockImplementation(() =>\n      Promise.resolve(\"contents\")\n    );\n\n    const fileTitleDeferred = createDeferred<string>();\n    const fileContentsDeferred = createDeferred<string>();\n\n    const { result } = renderHook(() =>\n      useYAMLPersistence({\n        title: \"title\",\n        yaml: \"\",\n        onFileOpened: (fileTitle, fileContents) => {\n          fileTitleDeferred.resolve(fileTitle);\n          fileContentsDeferred.resolve(fileContents);\n        },\n      })\n    );\n\n    result.current.open();\n\n    expect(fileTitleDeferred.promise).resolves.toBe(\"file\");\n    expect(fileContentsDeferred.promise).resolves.toBe(fileContents);\n  });\n});\n"
  },
  {
    "path": "src/persistence/use-yaml-persistence.ts",
    "content": "import { useCallback } from \"react\";\nimport { downloadFile, readFile, selectFile } from \"./file-management\";\n\ntype Props = {\n  title: string;\n  yaml: string;\n  onFileOpened: (fileTitle: string, fileContents: string) => void;\n};\n\nexport function useYAMLPersistence({\n  title,\n  yaml: contents,\n  onFileOpened,\n}: Props) {\n  const save = useCallback(() => {\n    const blob = new Blob([contents], { type: \"text/yaml\" });\n    downloadFile(title + \".yaml\", blob);\n  }, [title, contents]);\n\n  const open = useCallback(async () => {\n    const file = await selectFile(\"text/yaml\");\n\n    try {\n      const fileContents = await readFile(file);\n      const extStartIndex = file.name.lastIndexOf(\".\");\n      const fileTitle = file.name.slice(0, extStartIndex);\n\n      onFileOpened(fileTitle, fileContents);\n    } catch (e) {\n      console.error(\"Cannot read file: \", file.name, e);\n    }\n  }, [onFileOpened]);\n\n  return {\n    open,\n    save,\n  };\n}\n"
  },
  {
    "path": "src/rendering/debounced-queue.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { createDebouncedQueue } from \"./debounced-queue\";\nimport { createDeferred } from \"../utils\";\n\nconst ITEM1 = \"1\";\nconst ITEM2 = \"2\";\nconst ITEM3 = \"3\";\n\nconst DELAY = 200;\n\ntest(\"calls the callback with only the items pushed before the delay\", async () => {\n  const delay = 200;\n  const deferred = createDeferred<Array<string>>();\n\n  const queue = createDebouncedQueue<string>((items) => {\n    deferred.resolve(items);\n    return Promise.resolve();\n  }, DELAY);\n\n  queue.push(ITEM1);\n\n  // Add more items befire the delay\n  setTimeout(() => {\n    queue.push(ITEM2);\n  }, delay / 2);\n\n  // Add more items but after the delay\n  setTimeout(() => {\n    queue.push(ITEM3);\n  }, 2 * delay);\n\n  expect(deferred.promise).resolves.toStrictEqual([ITEM1, ITEM2]);\n});\n\ntest(\"calls the callback a one more time with the items pushed during the first callback\", async () => {\n  const deferred1 = createDeferred<Array<string>>();\n  const deferred2 = createDeferred<Array<string>>();\n  let deferred = deferred1;\n\n  const queue = createDebouncedQueue<string>((items) => {\n    deferred.resolve(items);\n    deferred = deferred2;\n\n    queue.push(ITEM2);\n    queue.push(ITEM3);\n    return Promise.resolve();\n  }, DELAY);\n\n  queue.push(ITEM1);\n  expect(deferred.promise).resolves.toStrictEqual([ITEM1]);\n  expect(deferred2.promise).resolves.toStrictEqual([ITEM2, ITEM3]);\n});\n"
  },
  {
    "path": "src/rendering/debounced-queue.ts",
    "content": "import { sleep } from \"../utils\";\n\nexport function createDebouncedQueue<T>(\n  callback: (items: Array<T>) => Promise<void>,\n  delay = 1000\n) {\n  let items: Array<T> = [];\n  let started = false;\n\n  function push(item: T) {\n    items.push(item);\n    if (!started) start();\n  }\n\n  async function start() {\n    started = true;\n\n    while (items.length) {\n      await sleep(delay);\n      const chunk = [...items];\n      items = [];\n      await callback(chunk);\n    }\n\n    started = false;\n  }\n\n  return { push };\n}\n"
  },
  {
    "path": "src/rendering/double-buffered.tsx",
    "content": "import { ReactElement, cloneElement, useEffect, useRef, useState } from \"react\";\n\ntype BufferElement = ReactElement | null;\n\ntype Props = {\n  render: (onSuccess: () => void) => BufferElement;\n};\n\nexport function DoubleBuffered({ render }: Props) {\n  const backElementRef = useRef<BufferElement>(null);\n  const frontElementRef = useRef<BufferElement>(null);\n  const [elements, setElements] = useState<null | Array<BufferElement>>(null);\n  const lastKeyRef = useRef(0);\n\n  // setElements and frontElementRef are stable, so no need for useCallback\n  const onRenderSuccess = () => {\n    frontElementRef.current = backElementRef.current\n      ? cloneElement(backElementRef.current, {\n          \"data-ready\": \"true\",\n        })\n      : null;\n\n    setElements([frontElementRef.current]);\n  };\n\n  useEffect(() => {\n    const renderedElement = render(onRenderSuccess);\n\n    if (renderedElement) {\n      const backElement = cloneElement(renderedElement, {\n        key: lastKeyRef.current,\n        \"data-ready\": \"false\",\n      });\n      backElementRef.current = backElement;\n      lastKeyRef.current++;\n      setElements([backElementRef.current, frontElementRef.current]);\n    } else {\n      backElementRef.current = null;\n      onRenderSuccess();\n    }\n  }, [render]);\n\n  return <div style={{ position: \"relative\" }}>{elements}</div>;\n}\n"
  },
  {
    "path": "src/rendering/index.ts",
    "content": "export * from \"./pdf\";\nexport * from \"./use-render\";\nexport * from \"./use-scale\";\n"
  },
  {
    "path": "src/rendering/multi-page-document.tsx",
    "content": "import { HTMLAttributes, useCallback, useMemo, useRef, useState } from \"react\";\nimport { Document, Page } from \"react-pdf\";\n\ntype Props = {\n  blob: Blob;\n  onAllPagesRenderSuccess: () => void;\n  scale?: number;\n} & HTMLAttributes<HTMLDivElement>;\n\nexport function MultiPageDocument({\n  blob,\n  scale = 1,\n  style,\n  onAllPagesRenderSuccess,\n  ...rest\n}: Props) {\n  const [pagesCount, setPagesCount] = useState(0);\n  const renderedPagesCountRef = useRef(0);\n\n  function onDocumentLoadSuccess({ numPages }: { numPages: number }) {\n    setPagesCount(numPages);\n  }\n\n  const onPageRenderSuccess = useCallback(() => {\n    renderedPagesCountRef.current++;\n    if (renderedPagesCountRef.current >= pagesCount) {\n      onAllPagesRenderSuccess();\n    }\n  }, [onAllPagesRenderSuccess, pagesCount]);\n\n  const pageElements = useMemo(() => {\n    const result = [];\n\n    for (let i = 1; i <= pagesCount; i++) {\n      result.push(\n        <div key={i}>\n          <Page\n            scale={scale}\n            loading={null}\n            onRenderSuccess={onPageRenderSuccess}\n            renderAnnotationLayer={false}\n            renderTextLayer={false}\n            pageNumber={i}\n          />\n        </div>,\n\n        <div key={`spacer-${i}`} style={{ height: \"1rem\" }} />\n      );\n    }\n\n    return result;\n  }, [pagesCount, onPageRenderSuccess, scale]);\n\n  return (\n    <div\n      data-testid=\"pdf-document\"\n      data-scale={scale}\n      style={{ position: \"absolute\", left: \"var(--left)\", ...style }}\n      {...rest}\n    >\n      <Document\n        loading={null}\n        onLoadSuccess={onDocumentLoadSuccess}\n        file={blob}\n      >\n        <div style={{ height: \"1rem\" }} />\n        {pageElements}\n      </Document>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/rendering/pdf.css",
    "content": ".PDF {\n  overflow-y: auto;\n  width: 100%;\n  height: 100%;\n  background-color: var(--color-gray-300);\n}\n"
  },
  {
    "path": "src/rendering/pdf.tsx",
    "content": "import { memo, useCallback, useEffect, useRef } from \"react\";\nimport { DoubleBuffered } from \"./double-buffered\";\nimport useResizeObserver from \"@react-hook/resize-observer\";\nimport \"./pdf.css\";\nimport { MultiPageDocument } from \"./multi-page-document\";\n\ntype Props = {\n  blob: Blob | null;\n  scale: number;\n};\n\nexport const PDF = memo(function ({ blob, scale }: Props) {\n  const ref = useRef<HTMLDivElement | null>(null);\n  const widthRef = useRef(0);\n\n  const update = useCallback(() => {\n    if (!ref.current) return;\n\n    const viewportWidth = widthRef.current;\n    const pageWidth = scale * 595;\n    const d = Math.abs((pageWidth - viewportWidth) / 2);\n    const viewportNode = ref.current;\n\n    if (pageWidth > viewportWidth) {\n      viewportNode.style.setProperty(\"--left\", \"0px\");\n      viewportNode.scroll(d, 0);\n    } else {\n      viewportNode.style.setProperty(\"--left\", `${d}px`);\n    }\n  }, [scale]);\n\n  useEffect(() => {\n    update();\n  }, [update]);\n\n  useResizeObserver(ref, (entry) => {\n    if (ref.current) {\n      widthRef.current = entry.contentRect.width;\n      update();\n    }\n  });\n\n  const render = useCallback(\n    (onSuccess: () => void) =>\n      blob && (\n        <MultiPageDocument\n          scale={scale}\n          onAllPagesRenderSuccess={onSuccess}\n          blob={blob}\n        />\n      ),\n    [scale, blob]\n  );\n\n  return (\n    <div ref={ref} data-testid=\"pdf\" className=\"PDF\">\n      <DoubleBuffered render={render} />\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/rendering/use-render.tsx",
    "content": "import { useCallback, useMemo, useRef, useState } from \"react\";\nimport { pdf } from \"@react-pdf/renderer\";\nimport { ResumeDocument } from \"../documents\";\nimport { Resume } from \"../types\";\nimport { sleep } from \"../utils\";\n\nfunction createDebouncedQueue<T>(\n  callback: (items: Array<T>) => Promise<void>,\n  delay = 1000\n) {\n  let items: Array<T> = [];\n  let started = false;\n\n  function push(item: T) {\n    items.push(item);\n    if (!started) start();\n  }\n\n  async function start() {\n    started = true;\n\n    while (items.length) {\n      await sleep(delay);\n      const chunk = [...items];\n      items = [];\n      await callback(chunk);\n    }\n\n    started = false;\n  }\n\n  return { push };\n}\n\nexport function useRender() {\n  const [blob, setBlob] = useState<Blob | null>(null);\n\n  const queueRef = useRef(\n    createDebouncedQueue(async (jsons: Array<Resume | null>) => {\n      const lastJSON = jsons[jsons.length - 1];\n\n      if (!lastJSON) {\n        setBlob(null);\n      } else {\n        try {\n          const newBlob = await pdf(\n            <ResumeDocument resume={lastJSON} />\n          ).toBlob();\n\n          setBlob(newBlob);\n        } catch (e) {\n          console.error(\"Cannot create PDF\");\n        }\n      }\n    }, 200)\n  );\n\n  const push = useCallback((json: Resume | null) => {\n    queueRef.current.push(json);\n  }, []);\n\n  const clear = useCallback(() => {\n    push(null);\n  }, [push]);\n\n  const queue = useMemo(() => ({ push, clear }), [push, clear]);\n\n  return { queue, blob, setBlob };\n}\n"
  },
  {
    "path": "src/rendering/use-scale.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { renderHook } from \"@testing-library/react\";\nimport { useScale, STORAGE_KEY, ABS_DELTA } from \".\";\nimport { act } from \"react-dom/test-utils\";\n\ntest(\"returns the initial scale if nothing is saved\", () => {\n  localStorage.removeItem(STORAGE_KEY);\n  const { result } = renderHook(() => useScale({ minScale: 0.5, maxScale: 2 }));\n  expect(result.current.scale).toBe(1);\n});\n\ntest(\"returns the value from the localStorage if present\", () => {\n  const scale = 1.2;\n  localStorage.setItem(STORAGE_KEY, scale.toString());\n  const { result } = renderHook(() => useScale({ minScale: 0.5, maxScale: 2 }));\n  expect(result.current.scale).toBe(1.2);\n});\n\ntest(\"increases the scale with the right delta and stores the value\", () => {\n  const { result } = renderHook(() => useScale({ minScale: 0.5, maxScale: 2 }));\n  const initialScale = result.current.scale;\n\n  act(() => {\n    result.current.zoomIn();\n  });\n\n  const newScale = initialScale + ABS_DELTA;\n  expect(result.current.scale).toBe(newScale);\n  expect(localStorage.getItem(STORAGE_KEY)).toEqual(newScale.toString());\n});\n\ntest(\"decreases the scale with the right delta and stores the value\", () => {\n  const { result } = renderHook(() => useScale({ minScale: 0.5, maxScale: 2 }));\n  const initialScale = result.current.scale;\n\n  act(() => {\n    result.current.zoomOut();\n  });\n\n  const newScale = initialScale - ABS_DELTA;\n  expect(result.current.scale).toBe(newScale);\n  expect(localStorage.getItem(STORAGE_KEY)).toEqual(newScale.toString());\n});\n\ntest(\"increases the scale no more than maxScale\", () => {\n  const maxScale = 1.2;\n  const initialScale = 1;\n  localStorage.setItem(\"scale\", initialScale.toString());\n\n  const { result } = renderHook(() =>\n    useScale({ minScale: 1, maxScale, absDelta: 0.1 })\n  );\n\n  act(() => {\n    result.current.zoomIn();\n    result.current.zoomIn();\n    result.current.zoomIn();\n    result.current.zoomIn();\n  });\n\n  expect(result.current.scale).toBe(maxScale);\n  expect(result.current.maxScaleReached).toBe(true);\n  expect(result.current.minScaleReached).toBe(false);\n});\n\ntest(\"decreases the scale no less than minScale\", () => {\n  const minScale = 1;\n  const initialScale = 1;\n  localStorage.setItem(\"scale\", initialScale.toString());\n\n  const { result } = renderHook(() =>\n    useScale({ minScale, maxScale: 1.2, absDelta: 0.1 })\n  );\n\n  act(() => {\n    result.current.zoomOut();\n    result.current.zoomOut();\n    result.current.zoomOut();\n    result.current.zoomOut();\n  });\n\n  expect(result.current.scale).toBe(minScale);\n  expect(result.current.minScaleReached).toBe(true);\n  expect(result.current.maxScaleReached).toBe(false);\n});\n"
  },
  {
    "path": "src/rendering/use-scale.ts",
    "content": "import { useCallback, useState } from \"react\";\nimport { clamp } from \"../utils/clamp\";\n\ntype Props = {\n  absDelta?: number;\n  minScale: number;\n  maxScale: number;\n};\n\nconst EPSILON = 0.00001;\n\nexport const ABS_DELTA = 0.1;\nexport const INITIAL_SCALE = 1;\nexport const STORAGE_KEY = \"scale\";\n\nexport function useScale({ minScale, maxScale, absDelta = ABS_DELTA }: Props) {\n  const [scale, setScale] = useState(\n    () => Number(localStorage.getItem(STORAGE_KEY)) || INITIAL_SCALE\n  );\n\n  const updateScale = useCallback(\n    (delta: number) => {\n      setScale((scale) => {\n        const newScale = clamp(minScale, maxScale, scale + delta);\n        localStorage.setItem(STORAGE_KEY, newScale.toString());\n\n        return newScale;\n      });\n    },\n    [minScale, maxScale]\n  );\n\n  const zoomIn = useCallback(() => {\n    updateScale(absDelta);\n  }, [updateScale, absDelta]);\n\n  const zoomOut = useCallback(() => {\n    updateScale(-absDelta);\n  }, [updateScale, absDelta]);\n\n  const maxScaleReached = Math.abs(scale - maxScale) < EPSILON;\n  const minScaleReached = Math.abs(scale - minScale) < EPSILON;\n\n  return {\n    scale,\n    zoomIn,\n    zoomOut,\n    maxScaleReached,\n    minScaleReached,\n  };\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "export type Job = {\n  name?: string;\n  location?: string;\n  position?: string;\n  url?: string;\n  summary?: string;\n  startDate?: string | number;\n  endDate?: string | number;\n  highlights?: Array<string>;\n};\n\nexport type Profile = {\n  network?: \"github\" | \"linkedin\";\n  url?: string;\n};\nexport type Location = { city?: string; countryCode?: string };\n\nexport type Basics = {\n  name?: string;\n  label?: string;\n  phone?: string;\n  email?: string;\n  url?: string;\n  summary?: string;\n  location?: Location;\n  profiles?: Array<Profile>;\n};\n\nexport type Skill = {\n  name?: string;\n  keywords?: Array<string>;\n};\n\nexport type Project = {\n  name?: string;\n  description?: string;\n  highlights?: Array<string | null>;\n  startDate?: string | number;\n  endDate?: string | number;\n  url?: string;\n};\n\nexport type EducationPlace = {\n  institution?: string;\n  url?: string;\n  area?: string;\n  score?: string;\n  courses?: Array<string | null>;\n  startDate?: string | number;\n  endDate?: string | number;\n};\n\nexport type Award = {\n  title?: string;\n  date?: string | number;\n  awarder?: string;\n  summary?: string;\n};\n\nexport type Certificate = {\n  name?: string;\n  date?: string | number;\n  url?: string;\n  issuer?: string;\n};\n\nexport type Publication = {\n  name?: string;\n  publisher?: string;\n  releaseDate?: string | number;\n  url?: string;\n  summary?: string;\n};\n\nexport type Voluteering = {\n  organization?: string;\n  url?: string;\n  position?: string;\n  summary?: string;\n  highlights?: Array<string | null>;\n  startDate?: string | number;\n  endDate?: string | number;\n};\n\nexport type Meta = {\n  accentColor?: string;\n  baseFontSize?: number;\n  sectionsOrder?: ResumeSectionName[];\n  sectionsPageBreaks?: ResumeSectionName[];\n};\n\nexport type Resume = {\n  basics?: Basics;\n  work?: Array<Job | null>;\n  skills?: Array<Skill | null>;\n  projects?: Array<Project | null>;\n  education?: Array<EducationPlace | null>;\n  awards?: Array<Award | null>;\n  certificates?: Array<Certificate | null>;\n  publications?: Array<Publication | null>;\n  volunteer?: Array<Voluteering | null>;\n  meta?: Meta;\n};\n\nexport type ResumeSectionName = Exclude<keyof Resume, \"meta\">;\n"
  },
  {
    "path": "src/utils/clamp.ts",
    "content": "export function clamp(min: number, max: number, value: number) {\n  return Math.min(Math.max(min, value), max);\n}\n"
  },
  {
    "path": "src/utils/deferred.ts",
    "content": "export function createDeferred<T>() {\n  let resolve: (value: T) => void = () => {};\n  let reject: (reason?: unknown) => void = () => {};\n\n  // The Promise is guarantted to call the passed callback synchronously\n  const promise = new Promise<T>((res, rej) => {\n    resolve = res;\n    reject = rej;\n  });\n\n  return { promise, resolve, reject };\n}\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "export * from \"./clamp\";\nexport * from \"./use-debounced-effect\";\nexport * from \"./sleep\";\nexport * from \"./deferred\";\n"
  },
  {
    "path": "src/utils/sleep.ts",
    "content": "export function sleep(delay: number = 1000) {\n  return new Promise((resolve) => setTimeout(resolve, delay));\n}\n"
  },
  {
    "path": "src/utils/use-debounced-effect.ts",
    "content": "import { useEffect } from \"react\";\n\nexport function useDebouncedEffect(effect: () => void) {\n  useEffect(() => {\n    const intervalId = setTimeout(effect, 200);\n\n    return () => {\n      clearTimeout(intervalId);\n    };\n  }, [effect]);\n}\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "/// <reference types=\"vitest\" />\n\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  test: {\n    globals: true,\n    include: [\"./src/**/*.test.*\"],\n    environment: \"jsdom\",\n  },\n});\n"
  }
]