Full Code of vangelov/devresume for AI

master a725666bb252 cached
114 files
123.3 KB
35.7k tokens
175 symbols
1 requests
Download .txt
Repository: vangelov/devresume
Branch: master
Commit: a725666bb252
Files: 114
Total size: 123.3 KB

Directory structure:
gitextract_1fnc526q/

├── .eslintrc.cjs
├── .gitignore
├── README.md
├── components.json
├── index.html
├── package.json
├── playwright/
│   ├── components/
│   │   ├── controls.ts
│   │   ├── editor.ts
│   │   ├── index.ts
│   │   └── pdf-document.ts
│   ├── controls.spec.ts
│   ├── sections.spec.ts
│   └── yaml.ts
├── playwright.config.ts
├── public/
│   ├── cache-sw.js
│   └── site.webmanifest
├── src/
│   ├── App.tsx
│   ├── app.css
│   ├── controls/
│   │   ├── controls-layout.css
│   │   ├── controls-layout.tsx
│   │   ├── file-controls.css
│   │   ├── file-controls.tsx
│   │   ├── index.ts
│   │   ├── preview-controls.css
│   │   ├── preview-controls.tsx
│   │   ├── title-controls.css
│   │   └── title-controls.tsx
│   ├── documents/
│   │   ├── bar.tsx
│   │   ├── document.tsx
│   │   ├── events-section.tsx
│   │   ├── fonts.ts
│   │   ├── grouped-section.tsx
│   │   ├── icons/
│   │   │   ├── github-icon.tsx
│   │   │   ├── globe-icon.tsx
│   │   │   ├── index.ts
│   │   │   ├── linkedin-icon.tsx
│   │   │   ├── location-icon.tsx
│   │   │   ├── mail-icon.tsx
│   │   │   ├── map-pin-icon.tsx
│   │   │   └── phone-icon.tsx
│   │   ├── index.ts
│   │   ├── rich-text.tsx
│   │   ├── section.tsx
│   │   ├── sections/
│   │   │   ├── awards-section.tsx
│   │   │   ├── basics-section/
│   │   │   │   ├── contacts.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── info-item.tsx
│   │   │   │   └── location-info-item.tsx
│   │   │   ├── certificates-section.tsx
│   │   │   ├── education-section.tsx
│   │   │   ├── index.ts
│   │   │   ├── projects-section.tsx
│   │   │   ├── publications-section.tsx
│   │   │   ├── skills-section.tsx
│   │   │   ├── volunteer-section.tsx
│   │   │   └── work-section.tsx
│   │   ├── stack.tsx
│   │   ├── svg-icon.tsx
│   │   ├── theme.ts
│   │   ├── use-has-page-break.ts
│   │   └── utils/
│   │       ├── format-date.test.ts
│   │       ├── format-date.ts
│   │       ├── get-sections-order.test.ts
│   │       ├── get-sections-order.ts
│   │       └── index.ts
│   ├── editing/
│   │   ├── index.ts
│   │   ├── schema.css
│   │   ├── schema.tsx
│   │   ├── type-highlighter.tsx
│   │   ├── yaml-editor.css
│   │   └── yaml-editor.tsx
│   ├── icons/
│   │   ├── download-icon.tsx
│   │   ├── folder-icon.tsx
│   │   ├── index.ts
│   │   ├── info-icon.tsx
│   │   ├── pdf-icon.tsx
│   │   ├── plus-icon.tsx
│   │   ├── zoom-in-icon.tsx
│   │   └── zoom-out-icon.tsx
│   ├── index.css
│   ├── main.tsx
│   ├── panes-layout.tsx
│   ├── parsing/
│   │   ├── index.ts
│   │   ├── parse-yaml.ts
│   │   ├── resume-schema.json
│   │   ├── sample.ts
│   │   ├── use-yaml-parsing.test.tsx
│   │   ├── use-yaml-parsing.ts
│   │   ├── validate-json.ts
│   │   └── yaml-to-json.ts
│   ├── persistence/
│   │   ├── file-management.ts
│   │   ├── index.ts
│   │   ├── use-yaml-persistence.test.ts
│   │   └── use-yaml-persistence.ts
│   ├── rendering/
│   │   ├── debounced-queue.test.ts
│   │   ├── debounced-queue.ts
│   │   ├── double-buffered.tsx
│   │   ├── index.ts
│   │   ├── multi-page-document.tsx
│   │   ├── pdf.css
│   │   ├── pdf.tsx
│   │   ├── use-render.tsx
│   │   ├── use-scale.test.ts
│   │   └── use-scale.ts
│   ├── types.ts
│   ├── utils/
│   │   ├── clamp.ts
│   │   ├── deferred.ts
│   │   ├── index.ts
│   │   ├── sleep.ts
│   │   └── use-debounced-effect.ts
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.cjs
================================================
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react-hooks/recommended",
  ],
  ignorePatterns: ["dist", ".eslintrc.cjs"],
  parser: "@typescript-eslint/parser",
  plugins: ["react-refresh"],
  rules: {
    "react-refresh/only-export-components": [
      "warn",
      { allowConstantExport: true },
    ],
  },
};


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/test-results/
/playwright-report/
/playwright/.cache/


================================================
FILE: README.md
================================================
<p align="center">
   <a href="https://devresume.app" target="_blank">
    <img src="screenshot.png" alt="Devices preview" />
  </a>
</p>
<h1 align="center">DevResume</h1>

<div align="center">

  <h3>Resume creator based on writing YAML with live preview and PDF export.</h3>
  
<br />

Website: https://devresume.app

**Completely free** • **No sign-up** • **Live preview** • **Works offline** • **Unlimited exports**

</div>

## Motivation

DevResume helps developers and technical people write their resume instead of wrestling with buttons and menus. 
The 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. 

## Scripts

In the project directory, you can run:

#### `npm run dev`
Runs the app in the development mode.\
Open [http://127.0.0.1:5173/](http://127.0.0.1:5173/) to view it in the browser.

#### `npm run build`
Builds the app for production in the `dist` folder.

#### `npm run test`
Runs the unit and integrations tests using Vitest.

#### `npm run test:e2e`
Runs the E2E tests using Playwright.




================================================
FILE: components.json
================================================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "src/index.css",
    "baseColor": "slate",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
    <link rel="manifest" href="/site.webmanifest" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DevResume</title>
  </head>
  <body spellcheck="false">
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "devresume",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite --host",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "test": "vitest",
    "test:e2e": "npx playwright test"
  },
  "dependencies": {
    "@codemirror/language": "^6.6.0",
    "@codemirror/legacy-modes": "^6.3.1",
    "@codemirror/lint": "^6.2.0",
    "@codemirror/stream-parser": "^0.19.9",
    "@react-hook/resize-observer": "^1.2.6",
    "@react-pdf/renderer": "3.1.12",
    "@uiw/codemirror-theme-github": "^4.19.9",
    "@uiw/codemirror-theme-vscode": "^4.21.18",
    "@uiw/react-codemirror": "^4.19.9",
    "markdown-to-jsx": "^7.3.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-pdf": "7.3.3",
    "split-pane-react": "^0.1.3",
    "yaml": "^2.3.2",
    "z-schema": "^6.0.1"
  },
  "devDependencies": {
    "@playwright/test": "^1.39.0",
    "@testing-library/react": "^14.0.0",
    "@types/codemirror": "^5.60.10",
    "@types/node": "^20.8.8",
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "jsdom": "^22.1.0",
    "typescript": "^5.0.2",
    "vite": "^4.4.5",
    "vitest": "^0.34.6"
  }
}


================================================
FILE: playwright/components/controls.ts
================================================
import { Page, expect } from "@playwright/test";

export function PreviewControls(page: Page) {
  const zoomIn = () => page.getByTestId("zoom-in").click();
  const zoomOut = () => page.getByTestId("zoom-out").click();
  const exportPDF = () => page.getByTestId("export").click();

  return {
    zoomIn,
    zoomOut,
    exportPDF,
  };
}

export function TitleControls(page: Page) {
  const input = page.getByTestId("title");

  const setTitle = (value: string) => input.fill(value);

  return {
    setTitle,
    expect: () => ({
      toHaveTitle: (title: string) => expect(input).toHaveValue(title),
    }),
  };
}

export function FileControls(page: Page) {
  const open = () => page.getByTestId("open").click();
  const save = () => page.getByTestId("save").click();
  const newResume = () => page.getByTestId("new").click();

  return {
    open,
    save,
    newResume,
  };
}


================================================
FILE: playwright/components/editor.ts
================================================
import { Page, expect } from "@playwright/test";

export function Editor(page: Page) {
  const self = page.locator(".cm-content");

  const type = async (value: string) => {
    await self.focus();
    await page.keyboard.insertText(value);
  };

  const clearAndRefresh = async () => {
    await page.evaluate(() => {
      localStorage["yaml"] = "";
    });
    await page.reload();
  };

  return {
    type,
    clearAndRefresh,
    expect: () => ({
      toHaveEmptyText: async () => {
        await expect
          .poll(async () => {
            const text = await self.innerText();
            return text.trim();
          })
          .toEqual("");
      },
    }),
  };
}


================================================
FILE: playwright/components/index.ts
================================================
export * from "./controls";
export * from "./pdf-document";
export * from "./editor";


================================================
FILE: playwright/components/pdf-document.ts
================================================
import { Page, expect } from "@playwright/test";

export function PDFDocument(page: Page) {
  // We need to check the data-ready attribute as well
  // in order to avoid getting the back buffer
  const self = page.locator('[data-testid="pdf-document"][data-ready="true"]');
  const pages = page.locator(".react-pdf__Page");

  const waitToAppear = () => self.waitFor({ state: "visible" });

  const getScale = async () => {
    const scale = await self.getAttribute("data-scale");
    return Number(scale);
  };

  const waitToZoomIn = async (initialScale: number) => {
    expect.poll(getScale).toBeGreaterThan(initialScale);
  };

  const waitToZoomOut = async (initialScale: number) => {
    expect.poll(getScale).toBeLessThan(initialScale);
  };

  return {
    self,
    waitToAppear,
    getScale,
    waitToZoomIn,
    waitToZoomOut,
    expect: () => ({
      ...expect(self),
      async toHaveScreenshotsOfPages() {
        for (const page of await pages.all()) {
          await expect(page).toHaveScreenshot({ scale: "device" });
        }
      },
    }),
  };
}


================================================
FILE: playwright/controls.spec.ts
================================================
import { test, expect } from "@playwright/test";
import { PDFDocument } from "./components/pdf-document";
import {
  Editor,
  FileControls,
  PreviewControls,
  TitleControls,
} from "./components";
import { SAMPLE_YAML } from "../src/parsing/sample";

test.describe("preview controls", () => {
  let pdfDocument;

  test.beforeEach(async ({ page }) => {
    await page.goto("/");
    pdfDocument = PDFDocument(page);
    await pdfDocument.waitToAppear();
  });

  test.describe("zooming", () => {
    let initialScale;

    test.beforeEach(async () => {
      initialScale = await pdfDocument.getScale();
    });

    test("should zoom-in", async ({ page }) => {
      await PreviewControls(page).zoomIn();
      await pdfDocument.waitToZoomIn(initialScale);
      await pdfDocument.expect().toHaveScreenshotsOfPages();
    });

    test("should zoom-out", async ({ page }) => {
      await PreviewControls(page).zoomOut();
      await pdfDocument.waitToZoomOut(initialScale);
      await pdfDocument.expect().toHaveScreenshotsOfPages();
    });
  });

  test("should export pdf", async ({ page }) => {
    const title = "TestTitle";
    await TitleControls(page).setTitle(title);
    const downloadPromise = page.waitForEvent("download");
    await PreviewControls(page).exportPDF();
    const download = await downloadPromise;

    expect(download.suggestedFilename()).toBe(title + ".pdf");
  });
});

test.describe("file controls", () => {
  test("should open .yml files", async ({ page }) => {
    await page.goto("/");
    await Editor(page).clearAndRefresh();

    const fileChooserPromise = page.waitForEvent("filechooser");
    await FileControls(page).open();
    const fileChooser = await fileChooserPromise;
    await fileChooser.setFiles({
      name: "sample.yml",
      mimeType: "text/yaml",
      buffer: Buffer.from(SAMPLE_YAML),
    });

    await TitleControls(page).expect().toHaveTitle("sample");

    const pdfDocument = PDFDocument(page);
    await pdfDocument.waitToAppear();
    await pdfDocument.expect().toHaveScreenshotsOfPages();
  });

  test("should save yaml", async ({ page }) => {
    await page.goto("/");
    const title = "TestTitle";
    await TitleControls(page).setTitle(title);
    const downloadPromise = page.waitForEvent("download");
    await FileControls(page).save();
    const download = await downloadPromise;

    expect(download.suggestedFilename()).toBe(title + ".yaml");
  });

  test("should create new resumes", async ({ page }) => {
    await page.goto("/");
    await FileControls(page).newResume();

    await Editor(page).expect().toHaveEmptyText();
    await PDFDocument(page).expect().toBeHidden();
  });
});


================================================
FILE: playwright/sections.spec.ts
================================================
import { test } from "@playwright/test";
import { Editor, PDFDocument } from "./components";
import {
  basicsYAML,
  workYAML,
  volunteerYAML,
  educationYAML,
  awardsYAML,
  publicationsYAML,
  skillsYAML,
  projectsYAML,
} from "./yaml";

const sections = [
  { name: "basics", yaml: basicsYAML },
  { name: "work", yaml: workYAML },
  { name: "volunteer", yaml: volunteerYAML },
  { name: "education", yaml: educationYAML },
  { name: "awards", yaml: awardsYAML },
  { name: "publications", yaml: publicationsYAML },
  { name: "skills", yaml: skillsYAML },
  { name: "projects", yaml: projectsYAML },
];

const orders = [
  {
    name: "default",
    sectionsOrder: [
      "basics",
      "skills",
      "work",
      "projects",
      "education",
      "awards",
      "certificates",
      "publications",
      "volunteer",
    ],
  },
  {
    name: "user-specified",
    sectionsOrder: [
      "basics",
      "volunteer",
      "work",
      "projects",

      "awards",
      "certificates",
      "skills",
      "publications",
      "education",
    ],
  },
];

for (const { name, yaml } of sections) {
  test(`should render section: ${name}`, async ({ page }) => {
    await page.goto("/");

    const editor = Editor(page);
    await editor.clearAndRefresh();
    await editor.type(yaml);

    const document = PDFDocument(page);
    await document.waitToAppear();
    await document.expect().toHaveScreenshotsOfPages();
  });
}

for (const { name, sectionsOrder } of orders) {
  test(`should render sections in the specified order: ${name}`, async ({
    page,
  }) => {
    await page.goto("/");

    const editor = Editor(page);
    await editor.clearAndRefresh();

    for (const { yaml } of sections) {
      await editor.type(yaml);
    }

    await editor.type(
      `meta:\n  sectionsOrder:\n${sectionsOrder
        .map((sectionName) => `    - ${sectionName}`)
        .join("\n")}`
    );

    const document = PDFDocument(page);
    await document.waitToAppear();
    await document.expect().toHaveScreenshotsOfPages();
  });
}

test(`should each section in sectionsPageBreaks on a new page`, async ({
  page,
}) => {
  await page.goto("/");

  const editor = Editor(page);
  await editor.clearAndRefresh();

  for (const { yaml } of sections) {
    await editor.type(yaml);
  }

  await editor.type(
    `meta:\n  sectionsPageBreaks:\n${sections
      .map(({ name }) => `    - ${name}`)
      .join("\n")}`
  );

  const document = PDFDocument(page);
  await document.waitToAppear();
  await document.expect().toHaveScreenshotsOfPages();
});


================================================
FILE: playwright/yaml.ts
================================================
export const basicsYAML = `
basics:
  name: Name1 Name2
  label: Label
  email: email@test.com
  phone: (912) 555-4321
  url: http://url.test.com
  summary: |
    Summary Line1
    Summary Line2
  location:
    city: City
    countryCode: Country
  profiles:
    - network: github
      url: github.com/test
    - network: linkedin
      url: linkedin.com/test
`;

export const workYAML = `
work:
  - name: Work1
    position: Position1
    location: Location1
    url: http://url1.example.com
    summary: 
      Summary1 **summary bold** *symmary italic* [summary link](https://link.test.com).
    startDate: 2010-12
    endDate: 2010-01
    highlights: 
      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).
      - 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.

  - name: Work2 
    position: Position2
    location: Location2
    url: http://url2.example.com
    summary: 
      Summary2 **summary bold** *symmary italic* [summary link](https://link.test.com).
    startDate: 2009-12
    endDate: 2009-01
    highlights: 
      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).
      - 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.
`;

export const volunteerYAML = `
volunteer:
  - organization: Organization1
    position: Position1
    url: http://url1.example.com/
    startDate: 2010-12
    endDate: 2010-01
    highlights:
      - Highlight 1.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).
      - 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.

  - organization: Organization2
    position: Position2
    url: http://url2.example.com/
    startDate: 2009-12
    endDate: 2009-01
    highlights:
      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).
      - 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.
`;

export const educationYAML = `
education:
  - institution: Institution1
    url: https://url1.test.com/
    area: Area1
    score: Score1 **score bold** *score italic* [score link](https://link.test.com).
    startDate: 2010-12
    endDate: 2010-01
    courses:
      - Course 1.1 **course bold** *course italic* [course link](https://link.test.com).
      - 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.
  
  - institution: Institution2
    url: https://url2.test.com/
    area: Area2
    score: Score2 **score bold** *score italic* [score link](https://link.test.com).
    startDate: 2009-12
    endDate: 2009-01
    courses:
      - Course 2.1 **course bold** *course italic* [course link](https://link.test.com).
      - 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.
`;

export const awardsYAML = `
awards:
  - title: Award1
    date: 2014-11
    awarder: Awarder1 
    summary: Summary 1 **summary bold** *summary italic* [summary link](https://link.test.com).

  - title: Award2
    date: 2012-02
    awarder: Awarder2
    summary: Summary 2 **summary bold** *summary italic* [summary link](https://link.test.com).
`;

export const publicationsYAML = `
publications:
  - name: Name1
    publisher: Publiser1
    releaseDate: 2014-12-06
    url: http://url1.test.com
    summary: Summary 1 **summary bold** *summary italic* [summary link](https://link.test.com).
   
  - name: Name2
    publisher: Publiser2
    releaseDate: 2010-10-10
    url: http://url2.test.com
    summary: Summary 2 **summary bold** *summary italic* [summary link](https://link.test.com).
`;

export const skillsYAML = `
skills:
  - name: Category1
    keywords: ["Skill 1.1", "Skill 1.2"]
    
  - name: Category2
    keywords: ["Skill 2.1"]
`;

export const projectsYAML = `
projects: 
  - name: Project1
    description: Description 1 **description bold** *description italic* [description link](https://link.test.com).
    startDate: 2010-12
    endDate: 2010-01
    url: url1.example.com
    highlights:
      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).
      - 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.

  - name: Project2
    description: Description 2 **description bold** *description italic* [description link](https://link.test.com).
    startDate: 2009-12
    endDate: 2009-01
    url: url1.example.com
    highlights:
      - Highlight 2.1 **highlight bold** *highlight italic* [hightlight link](https://link.test.com).
      - 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.
`;


================================================
FILE: playwright.config.ts
================================================
import { defineConfig, devices } from "@playwright/test";

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// require('dotenv').config();

/**
 * See https://playwright.dev/docs/test-configuration.
 */

const DEVICE_OVERRIDES = {
  viewport: {
    width: 1500,
    height: 1000,
  },
  deviceScaleFactor: 2,
};

export default defineConfig({
  testDir: "./playwright",

  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,

  retries: 0,

  workers: 3,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: "html",
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
    baseURL: "http://127.0.0.1:5173/",

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: "on-first-retry",
  },

  /* Configure projects for major browsers */
  projects: [
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
        ...DEVICE_OVERRIDES,
      },
    },

    {
      name: "firefox",
      use: {
        ...devices["Desktop Firefox"],
        ...DEVICE_OVERRIDES,
      },
    },

    {
      name: "webkit",
      use: {
        ...devices["Desktop Safari"],
        ...DEVICE_OVERRIDES,
      },
    },
  ],
});


================================================
FILE: public/cache-sw.js
================================================
self.addEventListener("activate", (event) => {
  console.log("SW: Activate", event);
  event.waitUntil(self.clients.claim());
});

self.addEventListener("install", () => {
  console.log("SW: Install");
});

const putInCache = async (request, response) => {
  const cache = await caches.open("v1");
  await cache.put(request, response);
};

const cacheFirst = async (request) => {
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }
  const responseFromNetwork = await fetch(request);
  putInCache(request, responseFromNetwork.clone());
  return responseFromNetwork;
};

self.addEventListener("fetch", (event) => {
  if (event.request.url.includes("pdf.worker")) {
    event.respondWith(cacheFirst(event.request));
  }
});


================================================
FILE: public/site.webmanifest
================================================
{
    "name": "",
    "short_name": "",
    "icons": [
       {
          "src": "/android-chrome-192x192.png",
          "sizes": "192x192",
          "type": "image/png"
       },
       {
        "src": "/android-chrome-512x512.png",
        "sizes": "512x512",
        "type": "image/png"
        }
    ],
    "theme_color": "#FFFFFF",
    "background_color": "#FFFFFF",
    "display": "standalone"
 }

================================================
FILE: src/App.tsx
================================================
import { RefObject, useCallback, useRef, useState } from "react";
import { PDF, useRender, useScale } from "./rendering";
import { Schema, YAMLEditor } from "./editing";
import "split-pane-react/esm/themes/default.css";
import {
  ControlsLayout,
  FileControls,
  PreviewControls,
  TitleControls,
} from "./controls";
import { PanesLayout } from "./panes-layout";
import "./app.css";
import { useYAMLPersistence, downloadFile } from "./persistence";
import { useYAMLParsing } from "./parsing";
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";

export function App() {
  const { queue, blob, setBlob } = useRender();
  const { zoomIn, zoomOut, scale, maxScaleReached, minScaleReached } = useScale(
    { minScale: 0.5, maxScale: 2 }
  );
  const [title, setTitle] = useState("Untitled");
  const codeMirrorRef: RefObject<ReactCodeMirrorRef> = useRef(null);

  // Parsing
  const onYAMLParsed = useCallback(
    (yaml: string, json: object | undefined) => {
      if (json) queue.push(json);
      else if (!yaml) queue.clear();
    },
    [queue]
  );
  const { setYAML, yaml } = useYAMLParsing({ onYAMLParsed });

  // Persistence
  const onFileOpened = useCallback(
    (fileTitle: string, fileContents: string) => {
      setTitle(fileTitle);
      setYAML(fileContents);
    },
    [setYAML]
  );
  const { save, open } = useYAMLPersistence({
    title,
    yaml: yaml,
    onFileOpened,
  });

  // Export
  const onDownload = useCallback(() => {
    if (blob) {
      downloadFile(title, blob);
    }
  }, [blob, title]);

  const onNewResume = useCallback(() => {
    setTitle("Untitled");
    setYAML("");
    setBlob(null);

    if (codeMirrorRef.current && codeMirrorRef.current.view) {
      codeMirrorRef.current.view.focus();
    }
  }, [setYAML, setBlob]);

  return (
    <div className="App">
      <ControlsLayout
        left={<FileControls onOpen={open} onSave={save} onNew={onNewResume} />}
        center={<TitleControls title={title} onChange={setTitle} />}
        right={
          <PreviewControls
            onDownload={onDownload}
            zoomInDisabled={maxScaleReached}
            zoomOutDisabled={minScaleReached}
            onZoomIn={zoomIn}
            onZoomOut={zoomOut}
          />
        }
      />

      <PanesLayout
        left={
          <YAMLEditor
            value={yaml}
            onChange={setYAML}
            codeMirrorRef={codeMirrorRef}
          />
        }
        right={<PDF scale={scale} blob={blob} />}
        bottom={<Schema />}
      />
    </div>
  );
}


================================================
FILE: src/app.css
================================================
.App {
  display: flex;
  position: relative;
  width: 100vw;
  height: 100vh;
  flex-direction: column;
}


================================================
FILE: src/controls/controls-layout.css
================================================
.ControlsLayout {
  display: flex;
  background-color: var(--color-gray-100);
  border-bottom: 1px solid var(--color-gray-400);
}


================================================
FILE: src/controls/controls-layout.tsx
================================================
import { ReactElement, cloneElement } from "react";
import "./controls-layout.css";

type Props = {
  left: ReactElement;
  center: ReactElement;
  right: ReactElement;
};

export function ControlsLayout({ left, center, right }: Props) {
  return (
    <div className="ControlsLayout">
      {cloneElement(left, { style: { flex: 1 } })}
      {cloneElement(center, { style: { flex: 1 } })}
      {cloneElement(right, { style: { flex: 1 } })}
    </div>
  );
}


================================================
FILE: src/controls/file-controls.css
================================================
.FileControls {
  gap: var(--space-3);
  display: flex;
  padding: var(--space-6);
  color: white;
}


================================================
FILE: src/controls/file-controls.tsx
================================================
import { CSSProperties } from "react";
import "./file-controls.css";
import { DownloadIcon, FolderIcon, InfoIcon, PlusIcon } from "../icons";

type Props = {
  onSave: () => void;
  onOpen: () => void;
  onNew: () => void;
  style?: CSSProperties;
};

export function FileControls({ onSave, onOpen, onNew, style }: Props) {
  return (
    <div className="FileControls" style={style}>
      <a
        title="About"
        data-testin="about"
        target="_blank"
        href="https://github.com/vangelov/devresume"
      >
        <InfoIcon size={16} />
      </a>

      <button data-testid="save" onClick={onSave}>
        <DownloadIcon size={14} style={{ marginRight: "0.5rem" }} />
        Save
      </button>

      <button data-testid="open" onClick={onOpen}>
        <FolderIcon size={14} style={{ marginRight: "0.5rem" }} />
        Open
      </button>

      <button data-testid="new" onClick={onNew}>
        <PlusIcon size={14} style={{ marginRight: "0.5rem" }} />
        New
      </button>
    </div>
  );
}


================================================
FILE: src/controls/index.ts
================================================
export * from "./controls-layout";
export * from "./preview-controls";
export * from "./title-controls";
export * from "./file-controls";


================================================
FILE: src/controls/preview-controls.css
================================================
.PreviewControls {
  display: flex;
  gap: var(--space-3);
  justify-content: flex-end;
  padding: var(--space-6);
}


================================================
FILE: src/controls/preview-controls.tsx
================================================
import { CSSProperties } from "react";
import "./preview-controls.css";
import { ZoomOutIcon, PDFIcon, ZoomInIcon } from "../icons";

type Props = {
  zoomInDisabled: boolean;
  zoomOutDisabled: boolean;
  onZoomIn: () => void;
  onZoomOut: () => void;
  onDownload: () => void;
  style?: CSSProperties;
};

export function PreviewControls({
  zoomInDisabled,
  zoomOutDisabled,
  onZoomIn,
  onZoomOut,
  onDownload,
  style,
}: Props) {
  return (
    <div className="PreviewControls" style={style}>
      <button
        disabled={zoomOutDisabled}
        data-testid="zoom-out"
        title="Zoom out"
        onClick={onZoomOut}
      >
        <ZoomOutIcon size={16} />
      </button>

      <button
        disabled={zoomInDisabled}
        data-testid="zoom-in"
        title="Zoom in"
        onClick={onZoomIn}
      >
        <ZoomInIcon size={16} />
      </button>

      <button className="primary" data-testid="export" onClick={onDownload}>
        <PDFIcon size={14} style={{ marginRight: "0.5rem" }} />
        Export
      </button>
    </div>
  );
}


================================================
FILE: src/controls/title-controls.css
================================================
.TitleControls {
  height: 100%;
  display: flex;
  justify-content: center;
  overflow-x: hidden;
  align-items: center;
}

.TitleControls-Input {
  width: 90%;
  color: white;
  font-weight: 600;
  background-color: transparent;
  border: none;
  text-align: center;
  font-size: var(--font-size-3);
}

.TitleControls-Input:focus {
  outline: var(--color-blue-100);
  outline-style: solid;
  outline-width: var(--space-1);
}


================================================
FILE: src/controls/title-controls.tsx
================================================
import { CSSProperties } from "react";
import "./title-controls.css";

type Props = {
  title: string;
  onChange: (value: string) => void;
  style?: CSSProperties;
};

export function TitleControls({ title, onChange, style }: Props) {
  return (
    <div className="TitleControls" style={style}>
      <input
        className="TitleControls-Input"
        data-testid="title"
        value={title}
        onChange={(event) => onChange(event.target.value)}
        onBlur={(event) => {
          if (!event.target.value) {
            onChange("Untitled");
          }
        }}
      />
    </div>
  );
}


================================================
FILE: src/documents/bar.tsx
================================================
import { View, StyleSheet } from "@react-pdf/renderer";
import { Theme } from "./theme";
import { useMemo } from "react";

type Props = {
  theme: Theme;
};

function createStyles(theme: Theme) {
  return StyleSheet.create({
    root: {
      backgroundColor: theme.color.accent,
      height: theme.space[4],
      position: "absolute",
      left: 0,
      right: 0,
      top: 0,
    },
  });
}

export function Bar({ theme }: Props) {
  const styles = useMemo(() => createStyles(theme), [theme]);
  return <View fixed style={styles.root} />;
}


================================================
FILE: src/documents/document.tsx
================================================
import { Resume } from "../types";
import { Page, Document, StyleSheet } from "@react-pdf/renderer";
import { BasicsSection } from "./sections/basics-section";
import {
  AwardsSection,
  CertificatesSection,
  EducationSection,
  ProjectsSection,
  PublicationsSection,
  SkillsSection,
  VolunteerSection,
  WorkSection,
} from "./sections";
import { Theme, createTheme } from "./theme";
import { Bar } from "./bar";
import { useMemo } from "react";
import { getSectionsOrder } from "./utils";
import { SectionProps } from "./section";
import { useHasPageBreak } from "./use-has-page-break";

type Props = {
  resume: Resume;
};

function createStyles(theme: Theme) {
  return StyleSheet.create({
    page: {
      backgroundColor: "white",
      fontFamily: "Roboto",
      paddingVertical: theme.space[10],
      paddingHorizontal: theme.space[12],
      fontSize: theme.fontSize[0],
      lineHeight: theme.lineHeight,
      color: theme.color.text,
    },
  });
}

export function ResumeDocument({ resume }: Props) {
  const {
    basics,
    work,
    skills,
    projects,
    education,
    awards,
    certificates,
    publications,
    volunteer,
    meta,
  } = resume;

  const accentColor = meta && meta.accentColor;
  const baseFontSize = meta && meta.baseFontSize;
  const sectionsOrder = useMemo(() => getSectionsOrder(meta), [meta]);

  const theme = useMemo(
    () => createTheme(accentColor, baseFontSize),
    [accentColor, baseFontSize]
  );
  const styles = createStyles(theme);

  const hasPageBreak = useHasPageBreak(meta);

  return (
    <Document>
      <Page style={styles.page} size="A4">
        <Bar theme={theme} />

        {sectionsOrder.map((sectionName) => {
          const commonProps: SectionProps = {
            theme,
            hasPageBreak: hasPageBreak(sectionName),
          };

          if (sectionName === "basics" && basics) {
            return (
              <BasicsSection
                {...commonProps}
                key={sectionName}
                basics={basics}
              />
            );
          }

          if (sectionName === "skills" && Array.isArray(skills)) {
            return (
              <SkillsSection
                {...commonProps}
                key={sectionName}
                skills={skills}
              />
            );
          }

          if (sectionName === "work" && Array.isArray(work)) {
            return (
              <WorkSection {...commonProps} key={sectionName} work={work} />
            );
          }

          if (sectionName === "projects" && Array.isArray(projects)) {
            return (
              <ProjectsSection
                {...commonProps}
                key={sectionName}
                projects={projects}
              />
            );
          }

          if (sectionName === "education" && Array.isArray(education)) {
            return (
              <EducationSection
                {...commonProps}
                key={sectionName}
                education={education}
              />
            );
          }

          if (sectionName === "awards" && Array.isArray(awards)) {
            return (
              <AwardsSection
                {...commonProps}
                key={sectionName}
                awards={awards}
              />
            );
          }

          if (sectionName === "certificates" && Array.isArray(certificates)) {
            return (
              <CertificatesSection
                {...commonProps}
                key={sectionName}
                certificates={certificates}
              />
            );
          }

          if (sectionName === "publications" && Array.isArray(publications)) {
            return (
              <PublicationsSection
                {...commonProps}
                key={sectionName}
                publications={publications}
              />
            );
          }

          if (sectionName === "volunteer" && Array.isArray(volunteer)) {
            return (
              <VolunteerSection
                {...commonProps}
                key={sectionName}
                volunteer={volunteer}
              />
            );
          }

          return null;
        })}
      </Page>
    </Document>
  );
}


================================================
FILE: src/documents/events-section.tsx
================================================
import { Text, Link } from "@react-pdf/renderer";
import { Section, SectionProps } from "./section";
import { HStack, VStack } from "./stack";
import { RichText } from "./rich-text";
import { Fragment, ReactElement, ReactNode } from "react";
import { formatDate } from "./utils";
import { Theme } from "./theme";

type EventHighlightItemProps = {
  children: string;
};

export function EventHighlightItem({ children }: EventHighlightItemProps) {
  return (
    <HStack wrap={false} style={{ alignItems: "flex-start" }} gap={3}>
      <Text>•</Text>
      <RichText>{children}</RichText>
    </HStack>
  );
}

//

type EventItemProps = {
  title?: string;
  url?: string;
  titleDetails?: Array<ReactElement>;
  description?: string;
  children?: ReactNode;
  startDate?: string | number;
  endDate?: string | number;
  date?: string | number;
  theme: Theme;
};

export function EventItem({
  title,
  url,
  children,
  description,
  titleDetails,
  startDate,
  endDate,
  date,
  theme,
}: EventItemProps) {
  return (
    <VStack wrap={false} gap={theme.space[2]}>
      <VStack gap={theme.space[1]}>
        <HStack
          style={{
            justifyContent: "space-between",
            alignItems: "flex-start",
          }}
        >
          <Text
            style={{
              marginRight: theme.space[5],
              flex: 1,
            }}
          >
            {url ? (
              <Link
                src={url}
                style={{
                  fontWeight: "medium",
                  color: theme.color.text,
                  textDecoration: "none",
                }}
              >
                {title}
              </Link>
            ) : (
              <Text style={{ fontWeight: "medium" }}>{title}</Text>
            )}

            {titleDetails &&
              titleDetails.map((titleDetail, index) => (
                <Fragment key={index}>
                  <Text style={{ color: theme.color.lightText }}> • </Text>
                  {titleDetail}
                </Fragment>
              ))}
          </Text>

          <Text style={{ color: theme.color.lightText }}>
            {date ? (
              formatDate(date)
            ) : startDate || endDate ? (
              <>
                {startDate && formatDate(startDate)}
                {endDate ? ` - ${formatDate(endDate)}` : " - Present"}
              </>
            ) : null}
          </Text>
        </HStack>

        {description && <RichText>{description}</RichText>}
      </VStack>

      {children && <VStack gap={theme.space[2]}>{children}</VStack>}
    </VStack>
  );
}

//

export type EventsSectionProps = SectionProps;

export function EventsSection({
  title,
  children,
  theme,
  ...rest
}: EventsSectionProps) {
  return (
    <Section theme={theme} title={title} {...rest}>
      <VStack gap={theme.space[8]}>{children}</VStack>
    </Section>
  );
}


================================================
FILE: src/documents/fonts.ts
================================================
import { Font } from "@react-pdf/renderer";

Font.register({
  family: "Roboto",
  fonts: [
    {
      src: "RobotoRegular.ttf",
      fontWeight: "normal",
    },

    {
      src: "RobotoMedium.ttf",
      fontWeight: "medium",
    },

    {
      src: "RobotoItalic.ttf",
      fontStyle: "italic",
    },
  ],
});


================================================
FILE: src/documents/grouped-section.tsx
================================================
import { Text, View } from "@react-pdf/renderer";
import { Section } from "./section";
import { HStack, VStack } from "./stack";
import { RichText } from "./rich-text";
import { EventsSectionProps } from "./events-section";

type GroupItemProps = {
  title?: string;
  description?: string;
};

export function GroupItem({ title, description }: GroupItemProps) {
  return (
    <HStack wrap={false} style={{ alignItems: "flex-start" }}>
      {title && (
        <Text style={{ flex: 0.6, fontWeight: "medium" }}>{title}</Text>
      )}

      {description ? (
        <View
          style={{
            flex: 1,
            display: "flex",
          }}
        >
          <RichText>{description}</RichText>
        </View>
      ) : null}
    </HStack>
  );
}

//

export type GroupedSectionProps = EventsSectionProps;

export function GroupedSection({
  title,
  children,
  theme,
  ...rest
}: GroupedSectionProps) {
  return (
    <Section theme={theme} title={title} {...rest}>
      <VStack gap={theme.space[7]}>{children}</VStack>
    </Section>
  );
}


================================================
FILE: src/documents/icons/github-icon.tsx
================================================
import { SvgIcon, SvgIconProps } from "../svg-icon";
import { Path } from "@react-pdf/renderer";

export function GitHubIcon({ color, ...rest }: SvgIconProps) {
  return (
    <SvgIcon {...rest}>
      <Path
        strokeWidth={2}
        strokeLineCap="round"
        stroke={color}
        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"
      />
    </SvgIcon>
  );
}


================================================
FILE: src/documents/icons/globe-icon.tsx
================================================
import { Circle, Line, Path } from "@react-pdf/renderer";
import { SvgIcon, SvgIconProps } from "../svg-icon";

export function GlobeIcon({ color, ...rest }: SvgIconProps) {
  return (
    <SvgIcon {...rest}>
      <Circle
        strokeWidth={2}
        stroke={color}
        strokeLineCap="round"
        cx="12"
        cy="12"
        r="10"
      />
      <Line
        strokeWidth={2}
        stroke={color}
        strokeLineCap="round"
        x1="2"
        y1="12"
        x2="22"
        y2="12"
      />
      <Path
        strokeWidth={2}
        stroke={color}
        strokeLineCap="round"
        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"
      />
    </SvgIcon>
  );
}


================================================
FILE: src/documents/icons/index.ts
================================================
export * from "./mail-icon";
export * from "./location-icon";
export * from "./github-icon";
export * from "./globe-icon";
export * from "./phone-icon";
export * from "./linkedin-icon";
export * from "./map-pin-icon";


================================================
FILE: src/documents/icons/linkedin-icon.tsx
================================================
import { Circle, Path, Rect } from "@react-pdf/renderer";
import { SvgIcon, SvgIconProps } from "../svg-icon";

export function LinkedInIcon({ color, ...rest }: SvgIconProps) {
  return (
    <SvgIcon {...rest}>
      <Path
        stroke={color}
        strokeWidth={2}
        strokeLineCap="round"
        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"
      />
      <Rect
        stroke={color}
        strokeWidth={2}
        strokeLineCap="round"
        x="2"
        y="9"
        width="4"
        height="12"
      />
      <Circle
        stroke={color}
        strokeWidth={2}
        strokeLineCap="round"
        cx="4"
        cy="4"
        r="2"
      />
    </SvgIcon>
  );
}


================================================
FILE: src/documents/icons/location-icon.tsx
================================================
import { Path, Circle } from "@react-pdf/renderer";
import { SvgIcon, SvgIconProps } from "../svg-icon";

export function LocationIcon({ color, ...rest }: SvgIconProps) {
  return (
    <SvgIcon {...rest}>
      <Path
        strokeWidth={2}
        strokeLineCap="round"
        stroke={color}
        d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"
      />
      <Circle stroke={color} strokeWidth={2} cx="12" cy="10" r="3"></Circle>
    </SvgIcon>
  );
}


================================================
FILE: src/documents/icons/mail-icon.tsx
================================================
import { Path, Polyline } from "@react-pdf/renderer";
import { SvgIconProps } from "../svg-icon";
import { SvgIcon } from "../svg-icon";

export function MailIcon({ color, ...rest }: SvgIconProps) {
  return (
    <SvgIcon {...rest}>
      <Path
        stroke={color}
        strokeWidth={2}
        strokeLineCap="round"
        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"
      />
      <Polyline
        stroke={color}
        strokeWidth={2}
        strokeLineCap="round"
        points="22,6 12,13 2,6"
      />
    </SvgIcon>
  );
}


================================================
FILE: src/documents/icons/map-pin-icon.tsx
================================================
import { Circle, Path } from "@react-pdf/renderer";
import { SvgIcon, SvgIconProps } from "../svg-icon";

export function MapPinIcon({ color, ...rest }: SvgIconProps) {
  return (
    <SvgIcon {...rest}>
      <Path
        strokeWidth={2}
        stroke={color}
        strokeLineCap="round"
        d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"
      />
      <Circle
        strokeWidth={2}
        strokeLineCap="round"
        stroke={color}
        cx="12"
        cy="10"
        r="3"
      />
    </SvgIcon>
  );
}


================================================
FILE: src/documents/icons/phone-icon.tsx
================================================
import { SvgIcon, SvgIconProps } from "../svg-icon";
import { Path } from "@react-pdf/renderer";

export function PhoneIcon({ color, ...rest }: SvgIconProps) {
  return (
    <SvgIcon {...rest}>
      <Path
        stroke={color}
        strokeWidth={2}
        strokeLineCap="round"
        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"
      />
    </SvgIcon>
  );
}


================================================
FILE: src/documents/index.ts
================================================
export * from "./document";
export * from "./fonts";


================================================
FILE: src/documents/rich-text.tsx
================================================
import { Text, Link } from "@react-pdf/renderer";
import Markdown from "markdown-to-jsx";
import { ReactNode } from "react";

type Props = {
  children?: ReactNode;
};

const TEXT_TYPES: Record<string, boolean | undefined> = {
  em: true,
  p: true,
  span: true,
  strong: true,
};

const STYLE_FOR_TEXT: Record<string, object> = {
  em: { fontStyle: "italic" },
  strong: { fontWeight: "medium" },
};

export function RichText({ children }: Props) {
  return (
    <Markdown
      children={`\n${children}`}
      options={{
        createElement(type, props, children) {
          const isText = TEXT_TYPES[type as string];

          if (isText) {
            const style = STYLE_FOR_TEXT[type as string];

            return (
              <Text key={props.key} style={style}>
                {children}
              </Text>
            );
          } else if (type === "a" && "href" in props) {
            return (
              <Link
                key={props.key}
                src={props.href as string}
                style={{ color: "black" }}
              >
                {children}
              </Link>
            );
          }

          return <Text key={props.key}>{children}</Text>;
        },
      }}
    />
  );
}


================================================
FILE: src/documents/section.tsx
================================================
import { ReactNode, useMemo } from "react";
import { Text, View, StyleSheet } from "@react-pdf/renderer";
import { Theme } from "./theme";

export type SectionProps = {
  title?: string;
  children?: ReactNode;
  theme: Theme;
  hasPageBreak?: boolean;
};

function createStyles(theme: Theme) {
  return StyleSheet.create({
    title: {
      fontSize: theme.fontSize[1],
      color: theme.color.accent,
      marginBottom: theme.space[5],
      fontWeight: "medium",
    },
    root: {
      marginBottom: theme.space[10],
    },
  });
}

export function Section({
  title,
  children,
  theme,
  hasPageBreak,
}: SectionProps) {
  const styles = useMemo(() => createStyles(theme), [theme]);

  return (
    <View style={styles.root} break={hasPageBreak}>
      {title && <Text style={styles.title}>{title}</Text>}
      {children}
    </View>
  );
}


================================================
FILE: src/documents/sections/awards-section.tsx
================================================
import { Award } from "../../types";
import { Text } from "@react-pdf/renderer";
import {
  EventItem,
  EventsSection,
  EventsSectionProps,
} from "../events-section";
import { ReactElement } from "react";
import { Theme } from "../theme";

export type AwardItemProps = {
  award: Award;
  theme: Theme;
};

export function AwardItem({ award, theme }: AwardItemProps) {
  const titleDetails: Array<ReactElement> = [];

  if (award.awarder) {
    titleDetails.push(<Text>{award.awarder}</Text>);
  }

  return (
    <EventItem
      theme={theme}
      title={award.title}
      description={award.summary}
      titleDetails={titleDetails}
      date={award.date}
    />
  );
}

//

export type AwardsSectionProps = {
  awards: Array<Award | null>;
} & EventsSectionProps;

export function AwardsSection({ awards, theme, ...rest }: AwardsSectionProps) {
  return (
    <EventsSection theme={theme} title="Awards" {...rest}>
      {awards.map(
        (award, index) =>
          award && <AwardItem key={index} theme={theme} award={award} />
      )}
    </EventsSection>
  );
}


================================================
FILE: src/documents/sections/basics-section/contacts.tsx
================================================
import { InfoItem } from "./info-item";
import {
  GitHubIcon,
  GlobeIcon,
  LinkedInIcon,
  MailIcon,
  PhoneIcon,
} from "../../icons";
import { VStack } from "../../stack";
import { Basics, Profile } from "../../../types";
import { Style } from "@react-pdf/types";
import { Theme } from "../../theme";
import { LocationInfoItem } from "./location-info-item";

export type Props = {
  basics: Basics;
  style: Style;
  theme: Theme;
};

function getProfiles(basics: Basics) {
  let linkedinProfile: Profile | null = null;
  let githubProfile: Profile | null = null;

  if (basics.profiles) {
    for (const profile of basics.profiles) {
      if (profile.network === "github") githubProfile = profile;
      else if (profile.network === "linkedin") linkedinProfile = profile;
    }
  }

  return { linkedinProfile, githubProfile };
}

export function Contacts({ basics, style, theme }: Props) {
  const { linkedinProfile, githubProfile } = getProfiles(basics);

  return (
    <VStack style={style} gap={2}>
      {basics.phone && (
        <InfoItem theme={theme} icon={<PhoneIcon />} value={basics.phone} />
      )}
      {basics.email && (
        <InfoItem theme={theme} icon={<MailIcon />} value={basics.email} />
      )}
      {basics.url && (
        <InfoItem theme={theme} icon={<GlobeIcon />} value={basics.url} />
      )}
      {githubProfile && githubProfile.url && (
        <InfoItem
          theme={theme}
          icon={<GitHubIcon />}
          value={githubProfile.url}
          href={githubProfile.url}
        />
      )}
      {linkedinProfile && linkedinProfile.url && (
        <InfoItem
          theme={theme}
          icon={<LinkedInIcon />}
          value={linkedinProfile.url}
          href={linkedinProfile.url}
        />
      )}
      {basics.location && (
        <LocationInfoItem theme={theme} location={basics.location} />
      )}
    </VStack>
  );
}


================================================
FILE: src/documents/sections/basics-section/index.tsx
================================================
import { Text, View, StyleSheet } from "@react-pdf/renderer";
import { Section, SectionProps } from "../../section";
import { HStack, VStack } from "../../stack";
import { Basics } from "../../../types";
import { RichText } from "../../rich-text";
import { Contacts } from "./contacts";
import { Theme } from "../../theme";
import { useMemo } from "react";

export type Props = {
  basics: Basics;
} & SectionProps;

function createStyles(theme: Theme) {
  return StyleSheet.create({
    name: { fontSize: theme.fontSize[2], fontWeight: "medium" },
    label: {
      color: theme.color.lightText,
      fontSize: theme.fontSize[1],
    },
  });
}

export function BasicsSection({ basics, theme, ...rest }: Props) {
  const styles = useMemo(() => createStyles(theme), [theme]);

  return (
    <Section theme={theme} {...rest}>
      <VStack gap={theme.space[5]}>
        <HStack>
          {basics.name && <Text style={styles.name}>{basics.name}</Text>}
          {basics.label && (
            <Text style={styles.label}>
              <Text> </Text>• {basics.label}
            </Text>
          )}
        </HStack>

        <HStack style={{ alignItems: "flex-start" }}>
          <Contacts theme={theme} style={{ flex: 0.6 }} basics={basics} />
          <View style={{ flex: 1 }}>
            {basics.summary && <RichText>{basics.summary}</RichText>}
          </View>
        </HStack>
      </VStack>
    </Section>
  );
}


================================================
FILE: src/documents/sections/basics-section/info-item.tsx
================================================
import { Text, Link, StyleSheet } from "@react-pdf/renderer";
import { ReactElement, cloneElement, useMemo } from "react";
import { HStack } from "../../stack";
import { Theme } from "../../theme";

type Props = {
  icon: ReactElement;
  value: string;
  href?: string;
  theme: Theme;
};

function createStyles(theme: Theme) {
  return StyleSheet.create({
    icon: {
      size: theme.fontSize[0],
      color: theme.color.lightText,
      style: { marginRight: theme.space[2] },
    },
    link: { color: theme.color.lightText, textDecoration: "none" },
  });
}

export function InfoItem({ icon, href, value, theme }: Props) {
  const styles = useMemo(() => createStyles(theme), [theme]);

  return (
    <HStack
      style={{
        color: theme.color.lightText,
      }}
    >
      {cloneElement(icon, styles.icon)}
      {href ? (
        <Link style={styles.link} src={href}>
          {value}
        </Link>
      ) : (
        <Text>{value}</Text>
      )}
    </HStack>
  );
}


================================================
FILE: src/documents/sections/basics-section/location-info-item.tsx
================================================
import { Location } from "../../../types";
import { MapPinIcon } from "../../icons";
import { Theme } from "../../theme";
import { InfoItem } from "./info-item";

type Props = {
  location: Location;
  theme: Theme;
};

export function LocationInfoItem({ location, theme }: Props) {
  const parts = [];
  const { city, countryCode } = location;

  if (city) parts.push(city);
  if (countryCode) parts.push(countryCode);

  const value = parts.join(", ");

  return <InfoItem theme={theme} icon={<MapPinIcon />} value={value} />;
}


================================================
FILE: src/documents/sections/certificates-section.tsx
================================================
import { Certificate } from "../../types";
import { Text } from "@react-pdf/renderer";
import { EventItem, EventsSection } from "../events-section";
import { ReactElement } from "react";
import { Theme } from "../theme";
import { SectionProps } from "../section";

export type CertificateItemProps = {
  certificate: Certificate;
  theme: Theme;
};

export function CerticateItem({ certificate, theme }: CertificateItemProps) {
  const titleDetails: Array<ReactElement> = [];

  if (certificate.issuer) {
    titleDetails.push(<Text>{certificate.issuer}</Text>);
  }

  return (
    <EventItem
      title={certificate.name}
      theme={theme}
      url={certificate.url}
      titleDetails={titleDetails}
      date={certificate.date}
    />
  );
}

//

type CertificatesSection = {
  certificates: Array<Certificate | null>;
} & SectionProps;

export function CertificatesSection({
  theme,
  certificates,
  ...rest
}: CertificatesSection) {
  return (
    <EventsSection theme={theme} title="Certificates" {...rest}>
      {certificates.map(
        (certificate, index) =>
          certificate && (
            <CerticateItem
              key={index}
              theme={theme}
              certificate={certificate}
            />
          )
      )}
    </EventsSection>
  );
}


================================================
FILE: src/documents/sections/education-section.tsx
================================================
import { EducationPlace } from "../../types";
import { Link } from "@react-pdf/renderer";
import {
  EventHighlightItem,
  EventItem,
  EventsSection,
  EventsSectionProps,
} from "../events-section";
import { ReactElement } from "react";
import { Theme } from "../theme";

export type EducationPlaceItemProps = {
  educationPlace: EducationPlace;
  theme: Theme;
};

export function EducationPlaceItem({
  educationPlace,
  theme,
}: EducationPlaceItemProps) {
  const titleDetails: Array<ReactElement> = [];

  if (educationPlace.institution) {
    titleDetails.push(
      <Link
        src={educationPlace.url || ""}
        style={{ color: theme.color.text, textDecoration: "none" }}
      >
        {educationPlace.institution}
      </Link>
    );
  }

  return (
    <EventItem
      title={educationPlace.area}
      description={educationPlace.score}
      titleDetails={titleDetails}
      startDate={educationPlace.startDate}
      endDate={educationPlace.endDate}
      theme={theme}
    >
      {educationPlace.courses &&
        Array.isArray(educationPlace.courses) &&
        educationPlace.courses.map(
          (course) =>
            course && (
              <EventHighlightItem key={course}>{course}</EventHighlightItem>
            )
        )}
    </EventItem>
  );
}

//

type EducationSectionProps = {
  education: Array<EducationPlace | null>;
} & EventsSectionProps;

export function EducationSection({
  education,
  theme,
  ...rest
}: EducationSectionProps) {
  return (
    <EventsSection theme={theme} title="Education" {...rest}>
      {education.map(
        (educationPlace, index) =>
          educationPlace && (
            <EducationPlaceItem
              key={index}
              theme={theme}
              educationPlace={educationPlace}
            />
          )
      )}
    </EventsSection>
  );
}


================================================
FILE: src/documents/sections/index.ts
================================================
export * from "./basics-section";
export * from "./work-section";
export * from "./education-section";
export * from "./skills-section";
export * from "./projects-section";
export * from "./awards-section";
export * from "./certificates-section";
export * from "./publications-section";
export * from "./volunteer-section";


================================================
FILE: src/documents/sections/projects-section.tsx
================================================
import { Project } from "../../types";
import {
  EventHighlightItem,
  EventItem,
  EventsSection,
  EventsSectionProps,
} from "../events-section";
import { Theme } from "../theme";

export type ProjectItemProps = {
  project: Project;
  theme: Theme;
};

export function ProjectItem({ project, theme }: ProjectItemProps) {
  return (
    <EventItem
      title={project.name}
      url={project.url}
      description={project.description}
      startDate={project.startDate}
      endDate={project.endDate}
      theme={theme}
    >
      {project.highlights &&
        Array.isArray(project.highlights) &&
        project.highlights.map(
          (hightlight) =>
            hightlight && (
              <EventHighlightItem key={hightlight}>
                {hightlight}
              </EventHighlightItem>
            )
        )}
    </EventItem>
  );
}

//

type ProjectsSectionProps = {
  projects: Array<Project | null>;
} & EventsSectionProps;

export function ProjectsSection({
  projects,
  theme,
  ...rest
}: ProjectsSectionProps) {
  return (
    <EventsSection theme={theme} title="Projects" {...rest}>
      {projects.map(
        (project, index) =>
          project && <ProjectItem key={index} theme={theme} project={project} />
      )}
    </EventsSection>
  );
}


================================================
FILE: src/documents/sections/publications-section.tsx
================================================
import { Publication } from "../../types";
import { Text } from "@react-pdf/renderer";
import {
  EventItem,
  EventsSection,
  EventsSectionProps,
} from "../events-section";
import { ReactElement } from "react";
import { Theme } from "../theme";

export type PublicationItemProps = {
  publication: Publication;
  theme: Theme;
};

export function PublicationItem({ publication, theme }: PublicationItemProps) {
  const titleDetails: Array<ReactElement> = [];
  const { name, url, publisher, releaseDate, summary } = publication;

  if (publisher) titleDetails.push(<Text>{publisher}</Text>);

  return (
    <EventItem
      title={name}
      description={summary}
      url={url}
      titleDetails={titleDetails}
      date={releaseDate}
      theme={theme}
    />
  );
}

//

type PublicationsSectionProps = {
  publications: Array<Publication | null>;
} & EventsSectionProps;

export function PublicationsSection({
  publications,
  theme,
  ...rest
}: PublicationsSectionProps) {
  return (
    <EventsSection theme={theme} title="Publications" {...rest}>
      {publications.map(
        (publication, index) =>
          publication && (
            <PublicationItem
              key={index}
              theme={theme}
              publication={publication}
            />
          )
      )}
    </EventsSection>
  );
}


================================================
FILE: src/documents/sections/skills-section.tsx
================================================
import { Skill } from "../../types";
import {
  GroupItem,
  GroupedSection,
  GroupedSectionProps,
} from "../grouped-section";

type SkillItemProps = {
  skill: Skill;
};

export function SkillItem({ skill }: SkillItemProps) {
  const { keywords } = skill;
  const description =
    keywords && Array.isArray(keywords) ? keywords.join(", ") : "";

  return <GroupItem title={skill.name} description={description} />;
}

//

type SkillsSectionProps = {
  skills: Array<Skill | null>;
} & GroupedSectionProps;

export function SkillsSection({ skills, theme, ...rest }: SkillsSectionProps) {
  return (
    <GroupedSection theme={theme} title="Skills" {...rest}>
      {skills.map(
        (skill, index) => skill && <SkillItem key={index} skill={skill} />
      )}
    </GroupedSection>
  );
}


================================================
FILE: src/documents/sections/volunteer-section.tsx
================================================
import { Voluteering } from "../../types";
import { Link } from "@react-pdf/renderer";
import {
  EventHighlightItem,
  EventItem,
  EventsSectionProps,
  EventsSection,
} from "../events-section";
import { ReactElement } from "react";
import { Theme } from "../theme";

export type VolunteeringItemProps = {
  volunteering: Voluteering;
  theme: Theme;
};

export function VolunteeringItem({
  volunteering,
  theme,
}: VolunteeringItemProps) {
  const titleDetails: Array<ReactElement> = [];

  if (volunteering.organization) {
    titleDetails.push(
      <Link
        src={volunteering.url || ""}
        style={{ color: theme.color.text, textDecoration: "none" }}
      >
        {volunteering.organization}
      </Link>
    );
  }

  return (
    <EventItem
      title={volunteering.position}
      description={volunteering.summary}
      titleDetails={titleDetails}
      startDate={volunteering.startDate}
      endDate={volunteering.endDate}
      theme={theme}
    >
      {volunteering.highlights &&
        Array.isArray(volunteering.highlights) &&
        volunteering.highlights.map(
          (highlight) =>
            highlight && (
              <EventHighlightItem key={highlight}>
                {highlight}
              </EventHighlightItem>
            )
        )}
    </EventItem>
  );
}

//

type VolunteerSectionProps = {
  volunteer: Array<Voluteering | null>;
} & EventsSectionProps;

export function VolunteerSection({ volunteer, theme }: VolunteerSectionProps) {
  return (
    <EventsSection theme={theme} title="Volunteer">
      {volunteer.map(
        (volunteering, index) =>
          volunteering && (
            <VolunteeringItem
              key={index}
              theme={theme}
              volunteering={volunteering}
            />
          )
      )}
    </EventsSection>
  );
}


================================================
FILE: src/documents/sections/work-section.tsx
================================================
import { Job } from "../../types";
import { Link, Text } from "@react-pdf/renderer";
import {
  EventHighlightItem,
  EventItem,
  EventsSectionProps,
  EventsSection,
} from "../events-section";
import { ReactElement } from "react";
import { Theme } from "../theme";

export type JobItemProps = {
  job: Job;
  theme: Theme;
};

export function JobItem({ job, theme }: JobItemProps) {
  const titleDetails: Array<ReactElement> = [];

  if (job.name) {
    titleDetails.push(
      <Link
        src={job.url || ""}
        style={{ color: theme.color.text, textDecoration: "none" }}
      >
        {job.name}
      </Link>
    );
  }

  if (job.location) {
    titleDetails.push(
      <Text style={{ color: theme.color.lightText }}>{job.location}</Text>
    );
  }

  return (
    <EventItem
      title={job.position}
      description={job.summary}
      titleDetails={titleDetails}
      startDate={job.startDate}
      endDate={job.endDate}
      theme={theme}
    >
      {job.highlights &&
        Array.isArray(job.highlights) &&
        job.highlights.map(
          (hightlight) =>
            hightlight && (
              <EventHighlightItem key={hightlight}>
                {hightlight}
              </EventHighlightItem>
            )
        )}
    </EventItem>
  );
}

//

type WorkSectionProps = {
  work: Array<Job | null>;
} & EventsSectionProps;

export function WorkSection({ work, theme, ...rest }: WorkSectionProps) {
  return (
    <EventsSection theme={theme} title="Work Experience" {...rest}>
      {work.map(
        (job, index) => job && <JobItem key={index} theme={theme} job={job} />
      )}
    </EventsSection>
  );
}


================================================
FILE: src/documents/stack.tsx
================================================
import { ReactNode } from "react";
import { View } from "@react-pdf/renderer";
import { Style, ViewProps } from "@react-pdf/types";

type Props = ViewProps & {
  gap?: number;
  children: ReactNode;
  flexWrap?: Style["flexWrap"];
  style?: Style;
};

export function HStack({ gap, children, flexWrap, style, ...rest }: Props) {
  const updatedStyle: Style = {
    display: "flex",
    flexDirection: "row",
    gap,
    flexWrap,
    alignItems: "center",
    ...style,
  };

  return (
    <View {...rest} style={updatedStyle}>
      {children}
    </View>
  );
}

export function VStack({ gap, children, flexWrap, style, ...rest }: Props) {
  const updatedStyle: Style = {
    display: "flex",
    flexDirection: "column",
    gap,
    flexWrap,
    ...style,
  };

  return (
    <View style={updatedStyle} {...rest}>
      {children}
    </View>
  );
}


================================================
FILE: src/documents/svg-icon.tsx
================================================
import { Svg } from "@react-pdf/renderer";
import { Style } from "@react-pdf/types";
import { ReactNode } from "react";

export type SvgIconProps = {
  size?: number;
  color?: string;
  style?: Style;
  children?: ReactNode;
};

export function SvgIcon({ size = 24, style, children }: SvgIconProps) {
  return (
    <Svg width={size} height={size} viewBox="0 0 24 24" style={style}>
      {children}
    </Svg>
  );
}


================================================
FILE: src/documents/theme.ts
================================================
export type Theme = {
  lineHeight: number;
  space: Array<number>;
  fontSize: Array<number>;
  color: {
    text: string;
    lightText: string;
    accent: string;
  };
};

export function createTheme(accentColor = "#2B5DD6", baseFontSize = 10) {
  const clampedBaseFontSize = Math.min(Math.max(10, baseFontSize), 16);

  const result: Theme = {
    //      0  1  2  3   4   5   6   7   8   9  10  11  12
    space: [2, 4, 6, 8, 10, 12, 14, 16, 20, 22, 24, 28, 32],

    lineHeight: 1.4,

    fontSize: [
      clampedBaseFontSize,
      1.4 * clampedBaseFontSize,
      1.8 * clampedBaseFontSize,
    ],
    color: {
      text: "black",
      lightText: "#6b7280",
      accent: accentColor,
    },
  };

  return result;
}


================================================
FILE: src/documents/use-has-page-break.ts
================================================
import { useCallback, useMemo } from "react";
import { Meta, ResumeSectionName } from "../types";

export function useHasPageBreak(meta?: Meta | null) {
  const sectionsPageBreaksSet = useMemo(
    () =>
      meta && Array.isArray(meta.sectionsPageBreaks)
        ? new Set<ResumeSectionName>(meta.sectionsPageBreaks)
        : null,
    [meta]
  );

  const hasPageBreak = useCallback(
    (sectionName: ResumeSectionName) => {
      return sectionsPageBreaksSet
        ? sectionsPageBreaksSet.has(sectionName)
        : false;
    },
    [sectionsPageBreaksSet]
  );

  return hasPageBreak;
}


================================================
FILE: src/documents/utils/format-date.test.ts
================================================
import { expect, test } from "vitest";
import { formatDate } from ".";

const tests = [
  [2021, "2021"],
  ["2021", "2021"],
  ["2021-2", "Feb 2021"],
  ["2021-02", "Feb 2021"],
  ["2021-02-01", "Feb 2021"],
  ["2021-", "2021"],
  ["2021-15", "Invalid Date 2021"],
  ["2021-0", "Invalid Date 2021"],
];

test("returns the right format", () => {
  for (const [input, expectation] of tests) {
    expect(formatDate(input)).toEqual(expectation);
  }
});


================================================
FILE: src/documents/utils/format-date.ts
================================================
export function formatDate(stringOrNumber: string | number): string | null {
  const dateString = stringOrNumber.toString();
  const parts = dateString.split("-").filter((part) => part !== "");
  const partsCount = parts.length;
  if (partsCount === 0) return null;

  const year = parts[0];
  if (partsCount === 1) return year;

  const date = new Date(dateString);
  const month = date.toLocaleString("en-US", { month: "short" });
  return `${month} ${year}`;
}


================================================
FILE: src/documents/utils/get-sections-order.test.ts
================================================
import { expect, test } from "vitest";
import { defaultSectionsOrder, getSectionsOrder } from "./get-sections-order";
import { ResumeSectionName } from "../../types";

test("returns the default order if no metadata", () => {
  const actual = getSectionsOrder();
  expect(actual).toEqual(defaultSectionsOrder);
});

test("returns the sections in the specified order ", () => {
  const sectionsOrder: ResumeSectionName[] = [
    "basics",
    "awards",
    "education",
    "volunteer",
    "skills",
    "work",
    "certificates",
    "projects",
    "publications",
  ];

  const actual = getSectionsOrder({
    sectionsOrder,
  });

  expect(actual).toEqual(sectionsOrder);
});

test("returns the specified sections in their order and the others in the default one", () => {
  const sectionsOrder: ResumeSectionName[] = ["basics", "work", "education"];

  const actual = getSectionsOrder({
    sectionsOrder,
  });

  expect(actual).toEqual([
    ...sectionsOrder,
    "skills",
    "projects",
    "awards",
    "certificates",
    "publications",
    "volunteer",
  ]);
});


================================================
FILE: src/documents/utils/get-sections-order.ts
================================================
import { Meta, ResumeSectionName } from "../../types";

export const defaultSectionsOrder: ResumeSectionName[] = [
  "basics",
  "skills",
  "work",
  "projects",
  "education",
  "awards",
  "certificates",
  "publications",
  "volunteer",
];

export function getSectionsOrder(meta?: Meta | null) {
  if (!meta || !Array.isArray(meta.sectionsOrder)) {
    return defaultSectionsOrder;
  }

  const { sectionsOrder } = meta;
  const finalSectionsOrder = [...sectionsOrder];

  const leftOutSections = defaultSectionsOrder.filter(
    (sectionName) => !sectionsOrder.includes(sectionName)
  );
  finalSectionsOrder.push(...leftOutSections);

  return finalSectionsOrder;
}


================================================
FILE: src/documents/utils/index.ts
================================================
export * from "./format-date";
export * from "./get-sections-order";


================================================
FILE: src/editing/index.ts
================================================
export * from "./yaml-editor";
export * from "./schema";


================================================
FILE: src/editing/schema.css
================================================
.Schema {
  background-color: var(--color-gray-100);
  border-top: 1px solid var(--color-gray-400);
  border-right: 1px solid var(--color-gray-400);

  width: 100%;
  height: 100%;
  color: var(--color-white);
  padding: var(--space-6);
  overflow: scroll;
}

.Schema-Keyword {
  color: var(--color-blue-300);
}

.Schema-Type {
  color: var(--color-green-100);
}

.Schema-Plain {
  color: var(--color-white);
}

.Schema-Field {
  color: var(--color-blue-400);
}

.Schema-Comment {
  color: var(--color-gray-600);
}


================================================
FILE: src/editing/schema.tsx
================================================
import { createTypeHighlighter } from "./type-highlighter";
import "./schema.css";
import { memo, useMemo } from "react";

function createHighlightedElements() {
  const highligher = createTypeHighlighter();

  highligher.pushType("Resume");

  highligher.pushObject("basics");
  highligher.addStringField("name");
  highligher.addStringField("label");
  highligher.addStringField("email");
  highligher.addStringField("summary", { markdown: true });
  highligher.pushArrayOfObjects("profiles");
  highligher.addEnumField("network", ["github", "network"]);
  highligher.pop();
  highligher.pop();

  highligher.pushArrayOfObjects("work");
  highligher.addStringField("name");
  highligher.addStringField("location");
  highligher.addStringField("position");
  highligher.addStringField("url");
  highligher.addStringField("summary");
  highligher.addStringField("startDate", { date: true });
  highligher.addStringField("endDate", { date: true });
  highligher.addArrayOfStringsField("highlights", { markdown: true });
  highligher.pop();

  highligher.pushArrayOfObjects("skills");
  highligher.addStringField("name");
  highligher.addArrayOfStringsField("keywords");
  highligher.pop();

  highligher.pushArrayOfObjects("work");
  highligher.addStringField("name");
  highligher.addStringField("description", { markdown: true });
  highligher.addStringField("url");
  highligher.addStringField("startDate", { date: true });
  highligher.addStringField("endDate", { date: true });
  highligher.addArrayOfStringsField("highlights", { markdown: true });
  highligher.pop();

  highligher.pushArrayOfObjects("education");
  highligher.addStringField("institution");
  highligher.addStringField("url");
  highligher.addStringField("area");
  highligher.addStringField("score", { markdown: true });
  highligher.addStringField("startDate", { date: true });
  highligher.addStringField("endDate", { date: true });
  highligher.addArrayOfStringsField("courses");
  highligher.pop();

  highligher.pushArrayOfObjects("awards");
  highligher.addStringField("title");
  highligher.addStringField("awarder");
  highligher.addStringField("date", { date: true });
  highligher.addStringField("summary", { markdown: true });
  highligher.pop();

  highligher.pushArrayOfObjects("certificates");
  highligher.addStringField("name");
  highligher.addStringField("url");
  highligher.addStringField("date", { date: true });
  highligher.addStringField("issuer");
  highligher.pop();

  highligher.pushArrayOfObjects("publications");
  highligher.addStringField("name");
  highligher.addStringField("publisher");
  highligher.addStringField("releaseDate", { date: true });
  highligher.addStringField("url");
  highligher.addStringField("summary", { markdown: true });
  highligher.pop();

  highligher.pushArrayOfObjects("volunteer");
  highligher.addStringField("organization");
  highligher.addStringField("position");
  highligher.addStringField("url");
  highligher.addStringField("summary", { markdown: true });
  highligher.addStringField("startDate", { date: true });
  highligher.addStringField("endDate", { date: true });
  highligher.addArrayOfStringsField("highlights", { markdown: true });
  highligher.pop();

  highligher.pushObject("meta");
  highligher.addStringField("accentColor");
  highligher.addNumberField("baseFontSize");
  highligher.addArrayOfStringsField("sectionsOrder", {
    comment: `how are sections ordered, e.g., ["basics", "work", "education", ...]`,
  });
  highligher.addArrayOfStringsField("sectionsPageBreaks", {
    comment: `which sections should start on a new page, e.g., ["basics", "work", "education", ...]`,
  });
  highligher.pop();

  highligher.pop();

  return highligher.result;
}

export const Schema = memo(function () {
  const highlightedElements = useMemo(createHighlightedElements, []);

  return (
    <div className="Schema">
      <pre
        style={{
          margin: 0,
          lineHeight: 1.5,
          fontFamily: "Monaco, Courier, monospace",
          fontSize: 14,
        }}
      >
        <span className="Schema-Comment">
          // Use this as a guide. It shows the supported fields from the JSON
          resume schema
          <br />
          // (jsonresume.org/schema) expressed in TypeScript so they're easier
          to read. <br />
          // -------------------------------------------------------------------
          <br />
          // Where noted you can use the following Markdown subset: <br />
          // *bold*, **italics**, [label](link). <br />
        </span>
        <br />

        {highlightedElements}
      </pre>
    </div>
  );
});


================================================
FILE: src/editing/type-highlighter.tsx
================================================
import { Fragment, ReactElement } from "react";

// eslint-disable-next-line react-refresh/only-export-components
const IDENT = "  ";

export function createTypeHighlighter() {
  const result: ReactElement[] = [];
  let space = "";
  let lastKey = 0;

  function pushType(name: string) {
    result.push(
      <Fragment key={lastKey++}>
        <span className="Schema-Keyword">type</span>{" "}
        <span className="Schema-Type">{name}</span>{" "}
        <span className="Schema-Plan">=</span>{" "}
        <span className="Schema-Plain">{"{"}</span>
        <br />
      </Fragment>
    );

    space += IDENT;
  }

  function pushObject(name: string) {
    result.push(
      <Fragment key={lastKey++}>
        <span className="Schema-Field">
          {space}
          {name}
        </span>
        ?: <span className="Schema-Plain">{"{"}</span>
        <br />
      </Fragment>
    );

    space += IDENT;
  }

  function pushArrayOfObjects(name: string) {
    result.push(
      <Fragment key={lastKey++}>
        <span className="Schema-Field">
          {space}
          {name}
        </span>
        ?: <span className="Schema-Type">Array</span>
        <span className="Schema-Plain">&lt;</span>
        <span className="Schema-Plain">{"{"}</span>
        <br />
      </Fragment>
    );

    space += IDENT;
  }

  function pop() {
    space = space.slice(0, space.length - IDENT.length);

    result.push(
      <Fragment key={lastKey++}>
        <span className="Schema-Plain">
          {space}
          {"}"}
        </span>
        <span className="Schema-Plain">;</span>
        <br />
      </Fragment>
    );
  }

  function renderFieldName(name: string) {
    return (
      <span className="Schema-Field">
        {space}
        {name}
      </span>
    );
  }

  function renderFieldEnd({
    markdown = false,
    date = false,
    comment,
  }: { markdown?: boolean; date?: boolean; comment?: string } = {}) {
    return (
      <>
        <span className="Schema-Plain">;</span>
        {markdown && (
          <span className="Schema-Comment"> // supports Markdown subset</span>
        )}
        {date && <span className="Schema-Comment"> // YYYY or YYYY-MM</span>}
        {comment && <span className="Schema-Comment"> // {comment}</span>}
        <br />
      </>
    );
  }

  function addStringField(
    name: string,
    {
      markdown = false,
      date = false,
    }: { markdown?: boolean; date?: boolean } = {}
  ) {
    result.push(
      <Fragment key={lastKey++}>
        {renderFieldName(name)}
        ?: <span className="Schema-Type">string</span>
        {renderFieldEnd({ markdown, date })}
      </Fragment>
    );
  }

  function addNumberField(name: string) {
    result.push(
      <Fragment key={lastKey++}>
        {renderFieldName(name)}
        ?: <span className="Schema-Type">number</span>
        {renderFieldEnd()}
      </Fragment>
    );
  }

  function addEnumField(name: string, values: Array<string>) {
    result.push(
      <Fragment key={lastKey++}>
        {renderFieldName(name)}
        ?:{" "}
        <span className="pl-s">
          {values.map((value) => `"${value}"`).join(" | ")}
        </span>
        {renderFieldEnd()}
      </Fragment>
    );
  }

  function addArrayOfStringsField(
    name: string,
    { markdown = false, comment }: { markdown?: boolean; comment?: string } = {}
  ) {
    result.push(
      <Fragment key={lastKey++}>
        {renderFieldName(name)}
        ?: <span className="Schema-Type">Array</span>
        <span className="Schema-Plain">&lt;</span>
        <span className="Schema-Type">string</span>
        <span className="Schema-Plain">&gt;</span>
        {renderFieldEnd({ markdown, comment })}
      </Fragment>
    );
  }

  return {
    pushObject,
    pushArrayOfObjects,
    addStringField,
    addArrayOfStringsField,
    pop,
    pushType,
    addEnumField,

    addNumberField,
    result,
  };
}


================================================
FILE: src/editing/yaml-editor.css
================================================
.YAMLEditor {
  font-size: var(--font-size-1);
  overflow-y: hidden;
  height: 100%;
  border-right: 1px solid var(--color-gray-400);
}


================================================
FILE: src/editing/yaml-editor.tsx
================================================
import CodeMirror, { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import * as yamlMode from "@codemirror/legacy-modes/mode/yaml";
import { StreamLanguage } from "@codemirror/language";
import { vscodeDarkInit } from "@uiw/codemirror-theme-vscode";
import { RefObject, memo } from "react";
import "./yaml-editor.css";

const yaml = StreamLanguage.define(yamlMode.yaml);

type Props = {
  value: string;
  onChange: (value: string) => void;
  codeMirrorRef: RefObject<ReactCodeMirrorRef>;
};

export const YAMLEditor = memo(function ({
  value,
  onChange,
  codeMirrorRef,
}: Props) {
  return (
    <CodeMirror
      ref={codeMirrorRef}
      className="YAMLEditor"
      value={value}
      onChange={onChange}
      theme={vscodeDarkInit({
        settings: { fontFamily: "Monaco, Courier, monospace" },
      })}
      extensions={[yaml]}
    />
  );
});


================================================
FILE: src/icons/download-icon.tsx
================================================
import { CSSProperties } from "react";

type Props = {
  size?: number;
  style?: CSSProperties;
};

export function DownloadIcon({ size = 24, style }: Props) {
  return (
    <svg
      width={size}
      height={size}
      style={style}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="feather feather-download"
    >
      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
      <polyline points="7 10 12 15 17 10"></polyline>
      <line x1="12" y1="15" x2="12" y2="3"></line>
    </svg>
  );
}


================================================
FILE: src/icons/folder-icon.tsx
================================================
import { CSSProperties } from "react";

type Props = {
  size?: number;
  style?: CSSProperties;
};

export function FolderIcon({ size = 24, style }: Props) {
  return (
    <svg
      width={size}
      height={size}
      style={style}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="feather feather-folder"
    >
      <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>
    </svg>
  );
}


================================================
FILE: src/icons/index.ts
================================================
export * from "./folder-icon";
export * from "./download-icon";
export * from "./pdf-icon";
export * from "./zoom-in-icon";
export * from "./zoom-out-icon";
export * from "./info-icon";
export * from "./plus-icon";


================================================
FILE: src/icons/info-icon.tsx
================================================
import { CSSProperties } from "react";

type Props = {
  size?: number;
  style?: CSSProperties;
};

export function InfoIcon({ size, style }: Props) {
  return (
    <svg
      width={size}
      height={size}
      style={style}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="feather feather-info"
    >
      <circle cx="12" cy="12" r="10"></circle>
      <line x1="12" y1="16" x2="12" y2="12"></line>
      <line x1="12" y1="8" x2="12.01" y2="8"></line>
    </svg>
  );
}


================================================
FILE: src/icons/pdf-icon.tsx
================================================
import { CSSProperties } from "react";

type Props = {
  size?: number;
  style?: CSSProperties;
};

export function PDFIcon({ size, style }: Props) {
  return (
    <svg
      width={size}
      height={size}
      style={style}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="feather feather-file-text"
    >
      <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
      <polyline points="14 2 14 8 20 8"></polyline>
      <line x1="16" y1="13" x2="8" y2="13"></line>
      <line x1="16" y1="17" x2="8" y2="17"></line>
      <polyline points="10 9 9 9 8 9"></polyline>
    </svg>
  );
}


================================================
FILE: src/icons/plus-icon.tsx
================================================
import { CSSProperties } from "react";

type Props = {
  size?: number;
  style?: CSSProperties;
};

export function PlusIcon({ size, style }: Props) {
  return (
    <svg
      width={size}
      height={size}
      style={style}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="feather feather-plus"
    >
      <line x1="12" y1="5" x2="12" y2="19"></line>
      <line x1="5" y1="12" x2="19" y2="12"></line>
    </svg>
  );
}


================================================
FILE: src/icons/zoom-in-icon.tsx
================================================
import { CSSProperties } from "react";

type Props = {
  size?: number;
  style?: CSSProperties;
};

export function ZoomInIcon({ size, style }: Props) {
  return (
    <svg
      width={size}
      height={size}
      style={style}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="feather feather-zoom-in"
    >
      <circle cx="11" cy="11" r="8"></circle>
      <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
      <line x1="11" y1="8" x2="11" y2="14"></line>
      <line x1="8" y1="11" x2="14" y2="11"></line>
    </svg>
  );
}


================================================
FILE: src/icons/zoom-out-icon.tsx
================================================
import { CSSProperties } from "react";

type Props = {
  size?: number;
  style?: CSSProperties;
};

export function ZoomOutIcon({ size, style }: Props) {
  return (
    <svg
      width={size}
      height={size}
      style={style}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="feather feather-zoom-out"
    >
      <circle cx="11" cy="11" r="8"></circle>
      <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
      <line x1="8" y1="11" x2="14" y2="11"></line>
    </svg>
  );
}


================================================
FILE: src/index.css
================================================
body {
  margin: 0;
}
body,
button,
input {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}

:root {
  --color-gray-100: #1e1e1e;
  --color-gray-200: #2a2a2a;
  --color-gray-300: #363636;
  --color-gray-400: #454545;
  --color-gray-500: #555555;
  --color-gray-600: #777777;

  --color-blue-100: #2b5dd6;
  --color-blue-200: #3b82f6;
  --color-blue-300: #569cd6;
  --color-blue-400: #9cdcfe;

  --color-green-100: #4ec9b0;

  --color-white: #ffffff;
  --color-black: #000000;

  --space-1: 0.125rem;
  --space-2: 0.375rem;
  --space-3: 0.5rem;
  --space-4: 0.625rem;
  --space-5: 0.875rem;
  --space-6: 1rem;
  --space-7: 1.25rem;
  --space-8: 1.375rem;

  --font-size-1: 0.875rem;
  --font-size-2: 1rem;
  --font-size-3: 1.25rem;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

.cm-editor {
  height: 100%;
}

::-webkit-scrollbar {
  width: var(--space-4);
  height: var(--space-4);
}

::-webkit-scrollbar-thumb {
  background: var(--color-gray-500);
}

::-webkit-scrollbar-thumb:hover {
  background: var(--color-gray-600);
}
::-webkit-scrollbar-thumb:active {
  background: var(--color-white);
}
::-webkit-scrollbar-corner {
  background: var(--color-gray-100);
}

button,
a {
  border: none;
  padding: var(--space-2) var(--space-8) var(--space-2);
  background-color: var(--color-gray-300);
  color: var(--color-white);
  cursor: pointer;
  font-size: var(--font-size-1);
}

button:hover,
a:hover {
  background-color: var(--color-gray-500);
}

button.primary {
  background-color: var(--color-blue-100);
}

button.primary:hover {
  background-color: var(--color-blue-200);
}

button:active,
button.primary:active,
a:active {
  color: var(--color-black);
  background-color: var(--color-white);
}

button:disabled,
a:disabled {
  opacity: 0.5;
  pointer-events: none;
}


================================================
FILE: src/main.tsx
================================================
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import "./index.css";
import { pdfjs } from "react-pdf";

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  "pdfjs-dist/build/pdf.worker.min.js",
  import.meta.url
).toString();

const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(
        "/cache-sw.js",
        {
          scope: "/",
        }
      );

      if (registration.active && !navigator.serviceWorker.controller) {
        window.location.reload();
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};

registerServiceWorker();

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);


================================================
FILE: src/panes-layout.tsx
================================================
import { ReactElement, useState } from "react";
import { Pane, SashContent } from "split-pane-react";
import SplitPane from "split-pane-react/esm/SplitPane";

type Props = {
  left: ReactElement;
  bottom: ReactElement;
  right: ReactElement;
};

function sashRender(_: number, active: boolean) {
  return <SashContent active={active} type="vscode" />;
}

export function PanesLayout({ left, bottom, right }: Props) {
  const [horizontalSizes, setHorizontalSizes] = useState<
    Array<string | number>
  >(["80%"]);

  const [verticalSizes, setVerticalSizes] = useState<Array<string | number>>([
    "50%",
  ]);

  return (
    <SplitPane
      sashRender={sashRender}
      split="vertical"
      sizes={verticalSizes}
      onChange={setVerticalSizes}
    >
      <Pane minSize="30%" maxSize="50%">
        <SplitPane
          sashRender={sashRender}
          split="horizontal"
          sizes={horizontalSizes}
          onChange={setHorizontalSizes}
        >
          {left}
          <Pane minSize="5%">{bottom}</Pane>
        </SplitPane>
      </Pane>

      {right}
    </SplitPane>
  );
}


================================================
FILE: src/parsing/index.ts
================================================
export * from "./use-yaml-parsing";


================================================
FILE: src/parsing/parse-yaml.ts
================================================
import YAML from "yaml";

type ParseResult = {
  json?: object;
  errorMessage?: string;
};

export function parseYAML(yaml: string): ParseResult {
  try {
    const json = YAML.parse(yaml, { prettyErrors: true });
    return { json };
  } catch (error) {
    return { errorMessage: (error as Error).message };
  }
}


================================================
FILE: src/parsing/resume-schema.json
================================================
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "additionalProperties": false,
  "definitions": {
    "iso8601": {
      "type": "string",
      "description": "Similar to the standard date type, but each section after the year is optional. e.g. 2014-06-29 or 2023-04",
      "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})$"
    }
  },
  "properties": {
    "$schema": {
      "type": "string",
      "description": "link to the version of the schema that can validate the resume",
      "format": "uri"
    },
    "basics": {
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "name": {
          "type": "string"
        },
        "label": {
          "type": "string",
          "description": "e.g. Web Developer"
        },
        "image": {
          "type": "string",
          "description": "URL (as per RFC 3986) to a image in JPEG or PNG format"
        },
        "email": {
          "type": "string",
          "description": "e.g. thomas@gmail.com",
          "format": "email"
        },
        "phone": {
          "type": "string",
          "description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923"
        },
        "url": {
          "type": "string",
          "description": "URL (as per RFC 3986) to your website, e.g. personal homepage",
          "format": "uri"
        },
        "summary": {
          "type": "string",
          "description": "Write a short 2-3 sentence biography about yourself"
        },
        "location": {
          "type": "object",
          "additionalProperties": true,
          "properties": {
            "address": {
              "type": "string",
              "description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li."
            },
            "postalCode": {
              "type": "string"
            },
            "city": {
              "type": "string"
            },
            "countryCode": {
              "type": "string",
              "description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN"
            },
            "region": {
              "type": "string",
              "description": "The general region where you live. Can be a US state, or a province, for instance."
            }
          }
        },
        "profiles": {
          "type": "array",
          "description": "Specify any number of social networks that you participate in",
          "additionalItems": false,
          "items": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "network": {
                "type": "string",
                "description": "e.g. Facebook or Twitter"
              },
              "username": {
                "type": "string",
                "description": "e.g. neutralthoughts"
              },
              "url": {
                "type": "string",
                "description": "e.g. http://twitter.example.com/neutralthoughts",
                "format": "uri"
              }
            }
          }
        }
      }
    },
    "work": {
      "type": "array",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "name": {
            "type": "string",
            "description": "e.g. Facebook"
          },
          "location": {
            "type": "string",
            "description": "e.g. Menlo Park, CA"
          },
          "description": {
            "type": "string",
            "description": "e.g. Social Media Company"
          },
          "position": {
            "type": "string",
            "description": "e.g. Software Engineer"
          },
          "url": {
            "type": "string",
            "description": "e.g. http://facebook.example.com",
            "format": "uri"
          },
          "startDate": {
            "$ref": "#/definitions/iso8601"
          },
          "endDate": {
            "$ref": "#/definitions/iso8601"
          },
          "summary": {
            "type": "string",
            "description": "Give an overview of your responsibilities at the company"
          },
          "highlights": {
            "type": "array",
            "description": "Specify multiple accomplishments",
            "additionalItems": false,
            "items": {
              "type": "string",
              "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
            }
          }
        }
      }
    },
    "volunteer": {
      "type": "array",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "organization": {
            "type": "string",
            "description": "e.g. Facebook"
          },
          "position": {
            "type": "string",
            "description": "e.g. Software Engineer"
          },
          "url": {
            "type": "string",
            "description": "e.g. http://facebook.example.com",
            "format": "uri"
          },
          "startDate": {
            "$ref": "#/definitions/iso8601"
          },
          "endDate": {
            "$ref": "#/definitions/iso8601"
          },
          "summary": {
            "type": "string",
            "description": "Give an overview of your responsibilities at the company"
          },
          "highlights": {
            "type": "array",
            "description": "Specify accomplishments and achievements",
            "additionalItems": false,
            "items": {
              "type": "string",
              "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
            }
          }
        }
      }
    },
    "education": {
      "type": "array",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "institution": {
            "type": "string",
            "description": "e.g. Massachusetts Institute of Technology"
          },
          "url": {
            "type": "string",
            "description": "e.g. http://facebook.example.com",
            "format": "uri"
          },
          "area": {
            "type": "string",
            "description": "e.g. Arts"
          },
          "studyType": {
            "type": "string",
            "description": "e.g. Bachelor"
          },
          "startDate": {
            "$ref": "#/definitions/iso8601"
          },
          "endDate": {
            "$ref": "#/definitions/iso8601"
          },
          "score": {
            "type": "string",
            "description": "grade point average, e.g. 3.67/4.0"
          },
          "courses": {
            "type": "array",
            "description": "List notable courses/subjects",
            "additionalItems": false,
            "items": {
              "type": "string",
              "description": "e.g. H1302 - Introduction to American history"
            }
          }
        }
      }
    },
    "awards": {
      "type": "array",
      "description": "Specify any awards you have received throughout your professional career",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "title": {
            "type": "string",
            "description": "e.g. One of the 100 greatest minds of the century"
          },
          "date": {
            "$ref": "#/definitions/iso8601"
          },
          "awarder": {
            "type": "string",
            "description": "e.g. Time Magazine"
          },
          "summary": {
            "type": "string",
            "description": "e.g. Received for my work with Quantum Physics"
          }
        }
      }
    },
    "certificates": {
      "type": "array",
      "description": "Specify any certificates you have received throughout your professional career",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "name": {
            "type": "string",
            "description": "e.g. Certified Kubernetes Administrator"
          },
          "date": {
            "$ref": "#/definitions/iso8601"
          },
          "url": {
            "type": "string",
            "description": "e.g. http://example.com",
            "format": "uri"
          },
          "issuer": {
            "type": "string",
            "description": "e.g. CNCF"
          }
        }
      }
    },
    "publications": {
      "type": "array",
      "description": "Specify your publications through your career",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "name": {
            "type": "string",
            "description": "e.g. The World Wide Web"
          },
          "publisher": {
            "type": "string",
            "description": "e.g. IEEE, Computer Magazine"
          },
          "releaseDate": {
            "$ref": "#/definitions/iso8601"
          },
          "url": {
            "type": "string",
            "description": "e.g. http://www.computer.org.example.com/csdl/mags/co/1996/10/rx069-abs.html",
            "format": "uri"
          },
          "summary": {
            "type": "string",
            "description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML."
          }
        }
      }
    },
    "skills": {
      "type": "array",
      "description": "List out your professional skill-set",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "name": {
            "type": "string",
            "description": "e.g. Web Development"
          },
          "level": {
            "type": "string",
            "description": "e.g. Master"
          },
          "keywords": {
            "type": "array",
            "description": "List some keywords pertaining to this skill",
            "additionalItems": false,
            "items": {
              "type": "string",
              "description": "e.g. HTML"
            }
          }
        }
      }
    },
    "languages": {
      "type": "array",
      "description": "List any other languages you speak",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "language": {
            "type": "string",
            "description": "e.g. English, Spanish"
          },
          "fluency": {
            "type": "string",
            "description": "e.g. Fluent, Beginner"
          }
        }
      }
    },
    "interests": {
      "type": "array",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "name": {
            "type": "string",
            "description": "e.g. Philosophy"
          },
          "keywords": {
            "type": "array",
            "additionalItems": false,
            "items": {
              "type": "string",
              "description": "e.g. Friedrich Nietzsche"
            }
          }
        }
      }
    },
    "references": {
      "type": "array",
      "description": "List references you have received",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "name": {
            "type": "string",
            "description": "e.g. Timothy Cook"
          },
          "reference": {
            "type": "string",
            "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."
          }
        }
      }
    },
    "projects": {
      "type": "array",
      "description": "Specify career projects",
      "additionalItems": false,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "name": {
            "type": "string",
            "description": "e.g. The World Wide Web"
          },
          "description": {
            "type": "string",
            "description": "Short summary of project. e.g. Collated works of 2017."
          },
          "highlights": {
            "type": "array",
            "description": "Specify multiple features",
            "additionalItems": false,
            "items": {
              "type": "string",
              "description": "e.g. Directs you close but not quite there"
            }
          },
          "keywords": {
            "type": "array",
            "description": "Specify special elements involved",
            "additionalItems": false,
            "items": {
              "type": "string",
              "description": "e.g. AngularJS"
            }
          },
          "startDate": {
            "$ref": "#/definitions/iso8601"
          },
          "endDate": {
            "$ref": "#/definitions/iso8601"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
          },
          "roles": {
            "type": "array",
            "description": "Specify your role on this project or in company",
            "additionalItems": false,
            "items": {
              "type": "string",
              "description": "e.g. Team Lead, Speaker, Writer"
            }
          },
          "entity": {
            "type": "string",
            "description": "Specify the relevant company/entity affiliations e.g. 'greenpeace', 'corporationXYZ'"
          },
          "type": {
            "type": "string",
            "description": " e.g. 'volunteering', 'presentation', 'talk', 'application', 'conference'"
          }
        }
      }
    },
    "meta": {
      "type": "object",
      "description": "The schema version and any other tooling configuration lives here",
      "additionalProperties": true,
      "properties": {
        "canonical": {
          "type": "string",
          "description": "URL (as per RFC 3986) to latest version of this document",
          "format": "uri"
        },
        "version": {
          "type": "string",
          "description": "A version field which follows semver - e.g. v1.0.0"
        },
        "lastModified": {
          "type": "string",
          "description": "Using ISO 8601 with YYYY-MM-DDThh:mm:ss"
        }
      }
    }
  },
  "title": "Resume Schema",
  "type": "object"
}


================================================
FILE: src/parsing/sample.ts
================================================
export const SAMPLE_YAML = `
# Demo resume adapted from https://github.com/jsonresume/resume-schema/blob/master/sample.resume.json

basics:
  name: Richard Hendriks
  label: Programmer
  email: richard.hendriks@mail.com
  phone: (912) 555-4321
  url: http://richardhendricks.example.com
  summary: 
    Richard hails from Tulsa. He has earned degrees from the 
    University of Oklahoma and Stanford. (Go Sooners and Cardinal!) 
    Before starting Pied Piper, he worked for Hooli as a part time 
    software developer. While his work focuses on applied information theory, 
    mostly optimizing lossless compression schema of both the length-limited 
    and adaptive variants, his non-work interests range widely, everything 
    from quantum computing to chaos theory. He could tell you about it, but 
    THAT would NOT be a “length-limited” conversation!
  location:
    city: San Francisco
    countryCode: US
  profiles:
    - network: github
      url: github.com/richardhendricks
    - network: linkedin
      url: linkedin.com/richardhendricks
work:
  - name: Pied Piper
    position: CEO/President
    location: Palo Alto, CA
    url: http://piedpiper.example.com
    summary: 
      Pied Piper is a multi-platform technology based on a proprietary 
      universal compression algorithm that has consistently fielded high
      Weisman Scores™ that are not merely competitive, but approach the 
      theoretical limit of lossless compression.
    startDate: 2013-12
    endDate: 2014-12
    highlights: 
      - Build an algorithm for artist to detect if their music was violating 
        copy right infringement laws
        
      - Successfully won Techcrunch Disrupt

      - Optimized an algorithm that holds the current world record for 
        Weisman Scores
         
volunteer:
  - organization: CoderDojo
    position: Teacher
    url: http://coderdojo.example.com/
    startDate: 2012-01
    endDate: 2013-01
    highlights:
      - Awarded 'Teacher of the Month'

education:
  - institution: University of Oklahoma
    url: https://www.ou.edu/
    area: Information Technology
    score: 'Score: 4.0'
    startDate: 2011-06
    endDate: 2014-01
    courses:
      - DB1101 - Basic SQL
      - CS2011 - Java Introduction

awards:
  - title: Digital Compression Pioneer Award
    date: 2014-11
    awarder: Techcrunch
    summary: There is no spoon.

publications:
  - name: Video compression for 3d media
    publisher: Hooli
    releaseDate: 2014-10
    url: http://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)
    summary: 
      Innovative middle-out compression algorithm that changes 
      the way we store data. 

skills:
  - name: Web Development
    keywords: ["HTML", "CSS", "Javascript"]
    
  - name: Compression
    keywords: ["Mpeg", "MP4", "GIF"]

projects: 
  - name: Miss Direction
    description: A mapping engine that misguides you
    startDate: 2016-08
    endDate: 2016-08
    url: missdirection.example.com
    highlights:
      - Won award at AIHacks 2016
      
      - Built by all women team of newbie programmers
      
      - Using modern technologies such as GoogleMaps, Chrome Extension 
        and Javascript
`;


================================================
FILE: src/parsing/use-yaml-parsing.test.tsx
================================================
import { describe, expect, test } from "vitest";
import { act, renderHook } from "@testing-library/react";
import { useYAMLParsing } from ".";
import { SAMPLE_YAML } from "./sample";
import { createDeferred } from "../utils";

const yaml = `
basics:
  name: Test
`;

const invalidYAML = `
basics
  name:
    test
`;

const json = { basics: { name: "Test" } };

//

test("returns the sample YAML if never used before", () => {
  const { result } = renderHook(() =>
    useYAMLParsing({ onYAMLParsed: () => {} })
  );
  expect(result.current.yaml).toEqual(SAMPLE_YAML);
});

test("updates the value in localStorage", () => {
  const yamlDeferred = createDeferred<string>();

  const { result } = renderHook(() =>
    useYAMLParsing({
      onYAMLParsed: (yaml) => {
        yamlDeferred.resolve(yaml);
      },
    })
  );

  act(() => {
    result.current.setYAML(yaml);
  });

  expect(yamlDeferred.promise).resolves.toBe(yaml);
});

describe("callback", () => {
  test("calls with the YAML and the parsed json if valid", async () => {
    const yamlDeferred = createDeferred<string>();
    const jsonDeferred = createDeferred<object | undefined>();

    const { result } = renderHook(() =>
      useYAMLParsing({
        onYAMLParsed: (yaml, json) => {
          yamlDeferred.resolve(yaml);
          jsonDeferred.resolve(json);
        },
      })
    );

    act(() => {
      result.current.setYAML(yaml);
    });

    expect(yamlDeferred.promise).resolves.toBe(yaml);
    expect(jsonDeferred.promise).resolves.toStrictEqual(json);
  });

  test("calls with the YAML and null if invalid", async () => {
    const yamlDeferred = createDeferred<string>();
    const jsonDeferred = createDeferred<object | undefined>();

    const { result } = renderHook(() =>
      useYAMLParsing({
        onYAMLParsed: (yaml, json) => {
          yamlDeferred.resolve(yaml);
          jsonDeferred.resolve(json);
        },
      })
    );

    act(() => {
      result.current.setYAML(invalidYAML);
    });

    expect(yamlDeferred.promise).resolves.toBe(invalidYAML);
    expect(jsonDeferred.promise).resolves.toBe(undefined);
  });
});


================================================
FILE: src/parsing/use-yaml-parsing.ts
================================================
import { useCallback, useState } from "react";
import { useDebouncedEffect } from "../utils";
import { yamlToJSON } from "./yaml-to-json";
import { SAMPLE_YAML } from "./sample";

type Props = {
  onYAMLParsed: (yaml: string, json: object | undefined) => void;
};

export const STORAGE_KEY = "yaml";

export function useYAMLParsing({ onYAMLParsed }: Props) {
  const [yaml, setYAML] = useState(() => {
    const stored = localStorage.getItem(STORAGE_KEY);

    if (!stored && stored !== "") {
      return SAMPLE_YAML;
    }

    return stored || "";
  });

  const onCodeUpdate = useCallback(() => {
    localStorage.setItem(STORAGE_KEY, yaml);
    const { json, errors } = yamlToJSON(yaml);

    if (process.env.NODE_ENV !== "test") {
      if (json) console.log("JSON:", json);
      if (errors) console.error("Errors:", errors);
    }

    onYAMLParsed(yaml, json);
  }, [yaml, onYAMLParsed]);

  useDebouncedEffect(onCodeUpdate);

  return {
    setYAML,
    yaml,
  };
}


================================================
FILE: src/parsing/validate-json.ts
================================================
// Validaiton

import Validator from "z-schema";
import resumeSchema from "./resume-schema.json";

const validator = new Validator({});

type ValidationResult = {
  errorMessages?: string[];
};

export function validateJSON(json: object): ValidationResult {
  validator.validate(json, resumeSchema);
  const errors = validator.getLastErrors();

  if (errors) {
    const errorMessages = errors.map(
      (error) => `${error.path}: ${error.message}`
    );
    return { errorMessages };
  }

  return {};
}


================================================
FILE: src/parsing/yaml-to-json.ts
================================================
import { parseYAML } from "./parse-yaml";
import { validateJSON } from "./validate-json";

type Result = {
  json?: object;
  errors?: {
    type: "parsing" | "validation";
    messages: string[];
  };
};

function resultForParseError(errorMessage: string): Result {
  return {
    errors: {
      type: "parsing",
      messages: [errorMessage],
    },
  };
}

function resultFromValidationErrors(errorsMessages: string[]): Result {
  return {
    errors: {
      type: "validation",
      messages: errorsMessages,
    },
  };
}

export function yamlToJSON(yaml: string): Result {
  const parseResult = parseYAML(yaml);

  if (parseResult.errorMessage)
    return resultForParseError(parseResult.errorMessage);

  if (parseResult.json && typeof parseResult.json === "object") {
    const validationResult = validateJSON(parseResult.json);

    if (validationResult.errorMessages)
      return resultFromValidationErrors(validationResult.errorMessages);

    return {
      json: parseResult.json,
    };
  }

  return {};
}


================================================
FILE: src/persistence/file-management.ts
================================================
export function downloadFile(fileName: string, blob: Blob) {
  const a: HTMLAnchorElement = document.createElement("a");
  a.style.display = "none";
  document.body.appendChild(a);

  const url = window.URL.createObjectURL(blob);
  a.href = url;
  a.download = fileName;
  a.click();
  a.remove();
  window.URL.revokeObjectURL(url);
}

export function readFile(file: File) {
  return new Promise<string>((resolve, reject) => {
    const fileReader = new FileReader();

    fileReader.onload = () => {
      const { result } = fileReader;
      resolve(result as string);
    };

    fileReader.onerror = () => {
      const { error } = fileReader;
      fileReader.abort();
      reject(error);
    };

    fileReader.readAsText(file);
  });
}

function createFileInput(accept: string) {
  const input = document.createElement("input");
  input.type = "file";
  input.multiple = false;
  input.accept = accept;

  return input;
}

export function selectFile(accept: string) {
  return new Promise<File>((resolve) => {
    const input = createFileInput(accept);

    input.addEventListener("change", function onChange(this: HTMLInputElement) {
      const { files } = this;

      if (files && files.length > 0) {
        resolve(files[0]);
      }
    });
    input.dispatchEvent(new MouseEvent("click"));
  });
}


================================================
FILE: src/persistence/index.ts
================================================
export * from "./use-yaml-persistence";
export * from "./file-management";


================================================
FILE: src/persistence/use-yaml-persistence.test.ts
================================================
import { test, expect, describe, afterEach } from "vitest";
import { renderHook } from "@testing-library/react";
import { useYAMLPersistence } from ".";
import * as fileManagement from "./file-management";
import { vi } from "vitest";
import { createDeferred } from "../utils";

describe("use-yaml-persistence", () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  test.only("saves the file with the right title", () => {
    const downloadFileSpy = vi
      .spyOn(fileManagement, "downloadFile")
      .mockImplementation(() => undefined);

    const { result } = renderHook(() =>
      useYAMLPersistence({ title: "title", yaml: "", onFileOpened: () => {} })
    );
    result.current.save();

    expect(downloadFileSpy.mock.calls[0][0]).toBe("title.yaml");
  });

  test("saves the file with the right title", () => {
    const fileName = "file.yaml";
    const fileContents = "contents";

    vi.spyOn(fileManagement, "selectFile").mockImplementation(() =>
      Promise.resolve({ name: fileName } as File)
    );

    vi.spyOn(fileManagement, "readFile").mockImplementation(() =>
      Promise.resolve("contents")
    );

    const fileTitleDeferred = createDeferred<string>();
    const fileContentsDeferred = createDeferred<string>();

    const { result } = renderHook(() =>
      useYAMLPersistence({
        title: "title",
        yaml: "",
        onFileOpened: (fileTitle, fileContents) => {
          fileTitleDeferred.resolve(fileTitle);
          fileContentsDeferred.resolve(fileContents);
        },
      })
    );

    result.current.open();

    expect(fileTitleDeferred.promise).resolves.toBe("file");
    expect(fileContentsDeferred.promise).resolves.toBe(fileContents);
  });
});


================================================
FILE: src/persistence/use-yaml-persistence.ts
================================================
import { useCallback } from "react";
import { downloadFile, readFile, selectFile } from "./file-management";

type Props = {
  title: string;
  yaml: string;
  onFileOpened: (fileTitle: string, fileContents: string) => void;
};

export function useYAMLPersistence({
  title,
  yaml: contents,
  onFileOpened,
}: Props) {
  const save = useCallback(() => {
    const blob = new Blob([contents], { type: "text/yaml" });
    downloadFile(title + ".yaml", blob);
  }, [title, contents]);

  const open = useCallback(async () => {
    const file = await selectFile("text/yaml");

    try {
      const fileContents = await readFile(file);
      const extStartIndex = file.name.lastIndexOf(".");
      const fileTitle = file.name.slice(0, extStartIndex);

      onFileOpened(fileTitle, fileContents);
    } catch (e) {
      console.error("Cannot read file: ", file.name, e);
    }
  }, [onFileOpened]);

  return {
    open,
    save,
  };
}


================================================
FILE: src/rendering/debounced-queue.test.ts
================================================
import { expect, test } from "vitest";
import { createDebouncedQueue } from "./debounced-queue";
import { createDeferred } from "../utils";

const ITEM1 = "1";
const ITEM2 = "2";
const ITEM3 = "3";

const DELAY = 200;

test("calls the callback with only the items pushed before the delay", async () => {
  const delay = 200;
  const deferred = createDeferred<Array<string>>();

  const queue = createDebouncedQueue<string>((items) => {
    deferred.resolve(items);
    return Promise.resolve();
  }, DELAY);

  queue.push(ITEM1);

  // Add more items befire the delay
  setTimeout(() => {
    queue.push(ITEM2);
  }, delay / 2);

  // Add more items but after the delay
  setTimeout(() => {
    queue.push(ITEM3);
  }, 2 * delay);

  expect(deferred.promise).resolves.toStrictEqual([ITEM1, ITEM2]);
});

test("calls the callback a one more time with the items pushed during the first callback", async () => {
  const deferred1 = createDeferred<Array<string>>();
  const deferred2 = createDeferred<Array<string>>();
  let deferred = deferred1;

  const queue = createDebouncedQueue<string>((items) => {
    deferred.resolve(items);
    deferred = deferred2;

    queue.push(ITEM2);
    queue.push(ITEM3);
    return Promise.resolve();
  }, DELAY);

  queue.push(ITEM1);
  expect(deferred.promise).resolves.toStrictEqual([ITEM1]);
  expect(deferred2.promise).resolves.toStrictEqual([ITEM2, ITEM3]);
});


================================================
FILE: src/rendering/debounced-queue.ts
================================================
import { sleep } from "../utils";

export function createDebouncedQueue<T>(
  callback: (items: Array<T>) => Promise<void>,
  delay = 1000
) {
  let items: Array<T> = [];
  let started = false;

  function push(item: T) {
    items.push(item);
    if (!started) start();
  }

  async function start() {
    started = true;

    while (items.length) {
      await sleep(delay);
      const chunk = [...items];
      items = [];
      await callback(chunk);
    }

    started = false;
  }

  return { push };
}


================================================
FILE: src/rendering/double-buffered.tsx
================================================
import { ReactElement, cloneElement, useEffect, useRef, useState } from "react";

type BufferElement = ReactElement | null;

type Props = {
  render: (onSuccess: () => void) => BufferElement;
};

export function DoubleBuffered({ render }: Props) {
  const backElementRef = useRef<BufferElement>(null);
  const frontElementRef = useRef<BufferElement>(null);
  const [elements, setElements] = useState<null | Array<BufferElement>>(null);
  const lastKeyRef = useRef(0);

  // setElements and frontElementRef are stable, so no need for useCallback
  const onRenderSuccess = () => {
    frontElementRef.current = backElementRef.current
      ? cloneElement(backElementRef.current, {
          "data-ready": "true",
        })
      : null;

    setElements([frontElementRef.current]);
  };

  useEffect(() => {
    const renderedElement = render(onRenderSuccess);

    if (renderedElement) {
      const backElement = cloneElement(renderedElement, {
        key: lastKeyRef.current,
        "data-ready": "false",
      });
      backElementRef.current = backElement;
      lastKeyRef.current++;
      setElements([backElementRef.current, frontElementRef.current]);
    } else {
      backElementRef.current = null;
      onRenderSuccess();
    }
  }, [render]);

  return <div style={{ position: "relative" }}>{elements}</div>;
}


================================================
FILE: src/rendering/index.ts
================================================
export * from "./pdf";
export * from "./use-render";
export * from "./use-scale";


================================================
FILE: src/rendering/multi-page-document.tsx
================================================
import { HTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
import { Document, Page } from "react-pdf";

type Props = {
  blob: Blob;
  onAllPagesRenderSuccess: () => void;
  scale?: number;
} & HTMLAttributes<HTMLDivElement>;

export function MultiPageDocument({
  blob,
  scale = 1,
  style,
  onAllPagesRenderSuccess,
  ...rest
}: Props) {
  const [pagesCount, setPagesCount] = useState(0);
  const renderedPagesCountRef = useRef(0);

  function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
    setPagesCount(numPages);
  }

  const onPageRenderSuccess = useCallback(() => {
    renderedPagesCountRef.current++;
    if (renderedPagesCountRef.current >= pagesCount) {
      onAllPagesRenderSuccess();
    }
  }, [onAllPagesRenderSuccess, pagesCount]);

  const pageElements = useMemo(() => {
    const result = [];

    for (let i = 1; i <= pagesCount; i++) {
      result.push(
        <div key={i}>
          <Page
            scale={scale}
            loading={null}
            onRenderSuccess={onPageRenderSuccess}
            renderAnnotationLayer={false}
            renderTextLayer={false}
            pageNumber={i}
          />
        </div>,

        <div key={`spacer-${i}`} style={{ height: "1rem" }} />
      );
    }

    return result;
  }, [pagesCount, onPageRenderSuccess, scale]);

  return (
    <div
      data-testid="pdf-document"
      data-scale={scale}
      style={{ position: "absolute", left: "var(--left)", ...style }}
      {...rest}
    >
      <Document
        loading={null}
        onLoadSuccess={onDocumentLoadSuccess}
        file={blob}
      >
        <div style={{ height: "1rem" }} />
        {pageElements}
      </Document>
    </div>
  );
}


================================================
FILE: src/rendering/pdf.css
================================================
.PDF {
  overflow-y: auto;
  width: 100%;
  height: 100%;
  background-color: var(--color-gray-300);
}


================================================
FILE: src/rendering/pdf.tsx
================================================
import { memo, useCallback, useEffect, useRef } from "react";
import { DoubleBuffered } from "./double-buffered";
import useResizeObserver from "@react-hook/resize-observer";
import "./pdf.css";
import { MultiPageDocument } from "./multi-page-document";

type Props = {
  blob: Blob | null;
  scale: number;
};

export const PDF = memo(function ({ blob, scale }: Props) {
  const ref = useRef<HTMLDivElement | null>(null);
  const widthRef = useRef(0);

  const update = useCallback(() => {
    if (!ref.current) return;

    const viewportWidth = widthRef.current;
    const pageWidth = scale * 595;
    const d = Math.abs((pageWidth - viewportWidth) / 2);
    const viewportNode = ref.current;

    if (pageWidth > viewportWidth) {
      viewportNode.style.setProperty("--left", "0px");
      viewportNode.scroll(d, 0);
    } else {
      viewportNode.style.setProperty("--left", `${d}px`);
    }
  }, [scale]);

  useEffect(() => {
    update();
  }, [update]);

  useResizeObserver(ref, (entry) => {
    if (ref.current) {
      widthRef.current = entry.contentRect.width;
      update();
    }
  });

  const render = useCallback(
    (onSuccess: () => void) =>
      blob && (
        <MultiPageDocument
          scale={scale}
          onAllPagesRenderSuccess={onSuccess}
          blob={blob}
        />
      ),
    [scale, blob]
  );

  return (
    <div ref={ref} data-testid="pdf" className="PDF">
      <DoubleBuffered render={render} />
    </div>
  );
});


================================================
FILE: src/rendering/use-render.tsx
================================================
import { useCallback, useMemo, useRef, useState } from "react";
import { pdf } from "@react-pdf/renderer";
import { ResumeDocument } from "../documents";
import { Resume } from "../types";
import { sleep } from "../utils";

function createDebouncedQueue<T>(
  callback: (items: Array<T>) => Promise<void>,
  delay = 1000
) {
  let items: Array<T> = [];
  let started = false;

  function push(item: T) {
    items.push(item);
    if (!started) start();
  }

  async function start() {
    started = true;

    while (items.length) {
      await sleep(delay);
      const chunk = [...items];
      items = [];
      await callback(chunk);
    }

    started = false;
  }

  return { push };
}

export function useRender() {
  const [blob, setBlob] = useState<Blob | null>(null);

  const queueRef = useRef(
    createDebouncedQueue(async (jsons: Array<Resume | null>) => {
      const lastJSON = jsons[jsons.length - 1];

      if (!lastJSON) {
        setBlob(null);
      } else {
        try {
          const newBlob = await pdf(
            <ResumeDocument resume={lastJSON} />
          ).toBlob();

          setBlob(newBlob);
        } catch (e) {
          console.error("Cannot create PDF");
        }
      }
    }, 200)
  );

  const push = useCallback((json: Resume | null) => {
    queueRef.current.push(json);
  }, []);

  const clear = useCallback(() => {
    push(null);
  }, [push]);

  const queue = useMemo(() => ({ push, clear }), [push, clear]);

  return { queue, blob, setBlob };
}


================================================
FILE: src/rendering/use-scale.test.ts
================================================
import { expect, test } from "vitest";
import { renderHook } from "@testing-library/react";
import { useScale, STORAGE_KEY, ABS_DELTA } from ".";
import { act } from "react-dom/test-utils";

test("returns the initial scale if nothing is saved", () => {
  localStorage.removeItem(STORAGE_KEY);
  const { result } = renderHook(() => useScale({ minScale: 0.5, maxScale: 2 }));
  expect(result.current.scale).toBe(1);
});

test("returns the value from the localStorage if present", () => {
  const scale = 1.2;
  localStorage.setItem(STORAGE_KEY, scale.toString());
  const { result } = renderHook(() => useScale({ minScale: 0.5, maxScale: 2 }));
  expect(result.current.scale).toBe(1.2);
});

test("increases the scale with the right delta and stores the value", () => {
  const { result } = renderHook(() => useScale({ minScale: 0.5, maxScale: 2 }));
  const initialScale = result.current.scale;

  act(() => {
    result.current.zoomIn();
  });

  const newScale = initialScale + ABS_DELTA;
  expect(result.current.scale).toBe(newScale);
  expect(localStorage.getItem(STORAGE_KEY)).toEqual(newScale.toString());
});

test("decreases the scale with the right delta and stores the value", () => {
  const { result } = renderHook(() => useScale({ minScale: 0.5, maxScale: 2 }));
  const initialScale = result.current.scale;

  act(() => {
    result.current.zoomOut();
  });

  const newScale = initialScale - ABS_DELTA;
  expect(result.current.scale).toBe(newScale);
  expect(localStorage.getItem(STORAGE_KEY)).toEqual(newScale.toString());
});

test("increases the scale no more than maxScale", () => {
  const maxScale = 1.2;
  const initialScale = 1;
  localStorage.setItem("scale", initialScale.toString());

  const { result } = renderHook(() =>
    useScale({ minScale: 1, maxScale, absDelta: 0.1 })
  );

  act(() => {
    result.current.zoomIn();
    result.current.zoomIn();
    result.current.zoomIn();
    result.current.zoomIn();
  });

  expect(result.current.scale).toBe(maxScale);
  expect(result.current.maxScaleReached).toBe(true);
  expect(result.current.minScaleReached).toBe(false);
});

test("decreases the scale no less than minScale", () => {
  const minScale = 1;
  const initialScale = 1;
  localStorage.setItem("scale", initialScale.toString());

  const { result } = renderHook(() =>
    useScale({ minScale, maxScale: 1.2, absDelta: 0.1 })
  );

  act(() => {
    result.current.zoomOut();
    result.current.zoomOut();
    result.current.zoomOut();
    result.current.zoomOut();
  });

  expect(result.current.scale).toBe(minScale);
  expect(result.current.minScaleReached).toBe(true);
  expect(result.current.maxScaleReached).toBe(false);
});


================================================
FILE: src/rendering/use-scale.ts
================================================
import { useCallback, useState } from "react";
import { clamp } from "../utils/clamp";

type Props = {
  absDelta?: number;
  minScale: number;
  maxScale: number;
};

const EPSILON = 0.00001;

export const ABS_DELTA = 0.1;
export const INITIAL_SCALE = 1;
export const STORAGE_KEY = "scale";

export function useScale({ minScale, maxScale, absDelta = ABS_DELTA }: Props) {
  const [scale, setScale] = useState(
    () => Number(localStorage.getItem(STORAGE_KEY)) || INITIAL_SCALE
  );

  const updateScale = useCallback(
    (delta: number) => {
      setScale((scale) => {
        const newScale = clamp(minScale, maxScale, scale + delta);
        localStorage.setItem(STORAGE_KEY, newScale.toString());

        return newScale;
      });
    },
    [minScale, maxScale]
  );

  const zoomIn = useCallback(() => {
    updateScale(absDelta);
  }, [updateScale, absDelta]);

  const zoomOut = useCallback(() => {
    updateScale(-absDelta);
  }, [updateScale, absDelta]);

  const maxScaleReached = Math.abs(scale - maxScale) < EPSILON;
  const minScaleReached = Math.abs(scale - minScale) < EPSILON;

  return {
    scale,
    zoomIn,
    zoomOut,
    maxScaleReached,
    minScaleReached,
  };
}


================================================
FILE: src/types.ts
================================================
export type Job = {
  name?: string;
  location?: string;
  position?: string;
  url?: string;
  summary?: string;
  startDate?: string | number;
  endDate?: string | number;
  highlights?: Array<string>;
};

export type Profile = {
  network?: "github" | "linkedin";
  url?: string;
};
export type Location = { city?: string; countryCode?: string };

export type Basics = {
  name?: string;
  label?: string;
  phone?: string;
  email?: string;
  url?: string;
  summary?: string;
  location?: Location;
  profiles?: Array<Profile>;
};

export type Skill = {
  name?: string;
  keywords?: Array<string>;
};

export type Project = {
  name?: string;
  description?: string;
  highlights?: Array<string | null>;
  startDate?: string | number;
  endDate?: string | number;
  url?: string;
};

export type EducationPlace = {
  institution?: string;
  url?: string;
  area?: string;
  score?: string;
  courses?: Array<string | null>;
  startDate?: string | number;
  endDate?: string | number;
};

export type Award = {
  title?: string;
  date?: string | number;
  awarder?: string;
  summary?: string;
};

export type Certificate = {
  name?: string;
  date?: string | number;
  url?: string;
  issuer?: string;
};

export type Publication = {
  name?: string;
  publisher?: string;
  releaseDate?: string | number;
  url?: string;
  summary?: string;
};

export type Voluteering = {
  organization?: string;
  url?: string;
  position?: string;
  summary?: string;
  highlights?: Array<string | null>;
  startDate?: string | number;
  endDate?: string | number;
};

export type Meta = {
  accentColor?: string;
  baseFontSize?: number;
  sectionsOrder?: ResumeSectionName[];
  sectionsPageBreaks?: ResumeSectionName[];
};

export type Resume = {
  basics?: Basics;
  work?: Array<Job | null>;
  skills?: Array<Skill | null>;
  projects?: Array<Project | null>;
  education?: Array<EducationPlace | null>;
  awards?: Array<Award | null>;
  certificates?: Array<Certificate | null>;
  publications?: Array<Publication | null>;
  volunteer?: Array<Voluteering | null>;
  meta?: Meta;
};

export type ResumeSectionName = Exclude<keyof Resume, "meta">;


================================================
FILE: src/utils/clamp.ts
================================================
export function clamp(min: number, max: number, value: number) {
  return Math.min(Math.max(min, value), max);
}


================================================
FILE: src/utils/deferred.ts
================================================
export function createDeferred<T>() {
  let resolve: (value: T) => void = () => {};
  let reject: (reason?: unknown) => void = () => {};

  // The Promise is guarantted to call the passed callback synchronously
  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });

  return { promise, resolve, reject };
}


================================================
FILE: src/utils/index.ts
================================================
export * from "./clamp";
export * from "./use-debounced-effect";
export * from "./sleep";
export * from "./deferred";


================================================
FILE: src/utils/sleep.ts
================================================
export function sleep(delay: number = 1000) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}


================================================
FILE: src/utils/use-debounced-effect.ts
================================================
import { useEffect } from "react";

export function useDebouncedEffect(effect: () => void) {
  useEffect(() => {
    const intervalId = setTimeout(effect, 200);

    return () => {
      clearTimeout(intervalId);
    };
  }, [effect]);
}


================================================
FILE: src/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: vite.config.ts
================================================
/// <reference types="vitest" />

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    include: ["./src/**/*.test.*"],
    environment: "jsdom",
  },
});
Download .txt
gitextract_1fnc526q/

├── .eslintrc.cjs
├── .gitignore
├── README.md
├── components.json
├── index.html
├── package.json
├── playwright/
│   ├── components/
│   │   ├── controls.ts
│   │   ├── editor.ts
│   │   ├── index.ts
│   │   └── pdf-document.ts
│   ├── controls.spec.ts
│   ├── sections.spec.ts
│   └── yaml.ts
├── playwright.config.ts
├── public/
│   ├── cache-sw.js
│   └── site.webmanifest
├── src/
│   ├── App.tsx
│   ├── app.css
│   ├── controls/
│   │   ├── controls-layout.css
│   │   ├── controls-layout.tsx
│   │   ├── file-controls.css
│   │   ├── file-controls.tsx
│   │   ├── index.ts
│   │   ├── preview-controls.css
│   │   ├── preview-controls.tsx
│   │   ├── title-controls.css
│   │   └── title-controls.tsx
│   ├── documents/
│   │   ├── bar.tsx
│   │   ├── document.tsx
│   │   ├── events-section.tsx
│   │   ├── fonts.ts
│   │   ├── grouped-section.tsx
│   │   ├── icons/
│   │   │   ├── github-icon.tsx
│   │   │   ├── globe-icon.tsx
│   │   │   ├── index.ts
│   │   │   ├── linkedin-icon.tsx
│   │   │   ├── location-icon.tsx
│   │   │   ├── mail-icon.tsx
│   │   │   ├── map-pin-icon.tsx
│   │   │   └── phone-icon.tsx
│   │   ├── index.ts
│   │   ├── rich-text.tsx
│   │   ├── section.tsx
│   │   ├── sections/
│   │   │   ├── awards-section.tsx
│   │   │   ├── basics-section/
│   │   │   │   ├── contacts.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── info-item.tsx
│   │   │   │   └── location-info-item.tsx
│   │   │   ├── certificates-section.tsx
│   │   │   ├── education-section.tsx
│   │   │   ├── index.ts
│   │   │   ├── projects-section.tsx
│   │   │   ├── publications-section.tsx
│   │   │   ├── skills-section.tsx
│   │   │   ├── volunteer-section.tsx
│   │   │   └── work-section.tsx
│   │   ├── stack.tsx
│   │   ├── svg-icon.tsx
│   │   ├── theme.ts
│   │   ├── use-has-page-break.ts
│   │   └── utils/
│   │       ├── format-date.test.ts
│   │       ├── format-date.ts
│   │       ├── get-sections-order.test.ts
│   │       ├── get-sections-order.ts
│   │       └── index.ts
│   ├── editing/
│   │   ├── index.ts
│   │   ├── schema.css
│   │   ├── schema.tsx
│   │   ├── type-highlighter.tsx
│   │   ├── yaml-editor.css
│   │   └── yaml-editor.tsx
│   ├── icons/
│   │   ├── download-icon.tsx
│   │   ├── folder-icon.tsx
│   │   ├── index.ts
│   │   ├── info-icon.tsx
│   │   ├── pdf-icon.tsx
│   │   ├── plus-icon.tsx
│   │   ├── zoom-in-icon.tsx
│   │   └── zoom-out-icon.tsx
│   ├── index.css
│   ├── main.tsx
│   ├── panes-layout.tsx
│   ├── parsing/
│   │   ├── index.ts
│   │   ├── parse-yaml.ts
│   │   ├── resume-schema.json
│   │   ├── sample.ts
│   │   ├── use-yaml-parsing.test.tsx
│   │   ├── use-yaml-parsing.ts
│   │   ├── validate-json.ts
│   │   └── yaml-to-json.ts
│   ├── persistence/
│   │   ├── file-management.ts
│   │   ├── index.ts
│   │   ├── use-yaml-persistence.test.ts
│   │   └── use-yaml-persistence.ts
│   ├── rendering/
│   │   ├── debounced-queue.test.ts
│   │   ├── debounced-queue.ts
│   │   ├── double-buffered.tsx
│   │   ├── index.ts
│   │   ├── multi-page-document.tsx
│   │   ├── pdf.css
│   │   ├── pdf.tsx
│   │   ├── use-render.tsx
│   │   ├── use-scale.test.ts
│   │   └── use-scale.ts
│   ├── types.ts
│   ├── utils/
│   │   ├── clamp.ts
│   │   ├── deferred.ts
│   │   ├── index.ts
│   │   ├── sleep.ts
│   │   └── use-debounced-effect.ts
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (175 symbols across 70 files)

FILE: playwright.config.ts
  constant DEVICE_OVERRIDES (line 13) | const DEVICE_OVERRIDES = {

FILE: playwright/components/controls.ts
  function PreviewControls (line 3) | function PreviewControls(page: Page) {
  function TitleControls (line 15) | function TitleControls(page: Page) {
  function FileControls (line 28) | function FileControls(page: Page) {

FILE: playwright/components/editor.ts
  function Editor (line 3) | function Editor(page: Page) {

FILE: playwright/components/pdf-document.ts
  function PDFDocument (line 3) | function PDFDocument(page: Page) {

FILE: src/App.tsx
  function App (line 17) | function App() {

FILE: src/controls/controls-layout.tsx
  type Props (line 4) | type Props = {
  function ControlsLayout (line 10) | function ControlsLayout({ left, center, right }: Props) {

FILE: src/controls/file-controls.tsx
  type Props (line 5) | type Props = {
  function FileControls (line 12) | function FileControls({ onSave, onOpen, onNew, style }: Props) {

FILE: src/controls/preview-controls.tsx
  type Props (line 5) | type Props = {
  function PreviewControls (line 14) | function PreviewControls({

FILE: src/controls/title-controls.tsx
  type Props (line 4) | type Props = {
  function TitleControls (line 10) | function TitleControls({ title, onChange, style }: Props) {

FILE: src/documents/bar.tsx
  type Props (line 5) | type Props = {
  function createStyles (line 9) | function createStyles(theme: Theme) {
  function Bar (line 22) | function Bar({ theme }: Props) {

FILE: src/documents/document.tsx
  type Props (line 21) | type Props = {
  function createStyles (line 25) | function createStyles(theme: Theme) {
  function ResumeDocument (line 39) | function ResumeDocument({ resume }: Props) {

FILE: src/documents/events-section.tsx
  type EventHighlightItemProps (line 9) | type EventHighlightItemProps = {
  function EventHighlightItem (line 13) | function EventHighlightItem({ children }: EventHighlightItemProps) {
  type EventItemProps (line 24) | type EventItemProps = {
  function EventItem (line 36) | function EventItem({
  type EventsSectionProps (line 108) | type EventsSectionProps = SectionProps;
  function EventsSection (line 110) | function EventsSection({

FILE: src/documents/grouped-section.tsx
  type GroupItemProps (line 7) | type GroupItemProps = {
  function GroupItem (line 12) | function GroupItem({ title, description }: GroupItemProps) {
  type GroupedSectionProps (line 35) | type GroupedSectionProps = EventsSectionProps;
  function GroupedSection (line 37) | function GroupedSection({

FILE: src/documents/icons/github-icon.tsx
  function GitHubIcon (line 4) | function GitHubIcon({ color, ...rest }: SvgIconProps) {

FILE: src/documents/icons/globe-icon.tsx
  function GlobeIcon (line 4) | function GlobeIcon({ color, ...rest }: SvgIconProps) {

FILE: src/documents/icons/linkedin-icon.tsx
  function LinkedInIcon (line 4) | function LinkedInIcon({ color, ...rest }: SvgIconProps) {

FILE: src/documents/icons/location-icon.tsx
  function LocationIcon (line 4) | function LocationIcon({ color, ...rest }: SvgIconProps) {

FILE: src/documents/icons/mail-icon.tsx
  function MailIcon (line 5) | function MailIcon({ color, ...rest }: SvgIconProps) {

FILE: src/documents/icons/map-pin-icon.tsx
  function MapPinIcon (line 4) | function MapPinIcon({ color, ...rest }: SvgIconProps) {

FILE: src/documents/icons/phone-icon.tsx
  function PhoneIcon (line 4) | function PhoneIcon({ color, ...rest }: SvgIconProps) {

FILE: src/documents/rich-text.tsx
  type Props (line 5) | type Props = {
  constant TEXT_TYPES (line 9) | const TEXT_TYPES: Record<string, boolean | undefined> = {
  constant STYLE_FOR_TEXT (line 16) | const STYLE_FOR_TEXT: Record<string, object> = {
  function RichText (line 21) | function RichText({ children }: Props) {

FILE: src/documents/section.tsx
  type SectionProps (line 5) | type SectionProps = {
  function createStyles (line 12) | function createStyles(theme: Theme) {
  function Section (line 26) | function Section({

FILE: src/documents/sections/awards-section.tsx
  type AwardItemProps (line 11) | type AwardItemProps = {
  function AwardItem (line 16) | function AwardItem({ award, theme }: AwardItemProps) {
  type AwardsSectionProps (line 36) | type AwardsSectionProps = {
  function AwardsSection (line 40) | function AwardsSection({ awards, theme, ...rest }: AwardsSectionProps) {

FILE: src/documents/sections/basics-section/contacts.tsx
  type Props (line 15) | type Props = {
  function getProfiles (line 21) | function getProfiles(basics: Basics) {
  function Contacts (line 35) | function Contacts({ basics, style, theme }: Props) {

FILE: src/documents/sections/basics-section/index.tsx
  type Props (line 10) | type Props = {
  function createStyles (line 14) | function createStyles(theme: Theme) {
  function BasicsSection (line 24) | function BasicsSection({ basics, theme, ...rest }: Props) {

FILE: src/documents/sections/basics-section/info-item.tsx
  type Props (line 6) | type Props = {
  function createStyles (line 13) | function createStyles(theme: Theme) {
  function InfoItem (line 24) | function InfoItem({ icon, href, value, theme }: Props) {

FILE: src/documents/sections/basics-section/location-info-item.tsx
  type Props (line 6) | type Props = {
  function LocationInfoItem (line 11) | function LocationInfoItem({ location, theme }: Props) {

FILE: src/documents/sections/certificates-section.tsx
  type CertificateItemProps (line 8) | type CertificateItemProps = {
  function CerticateItem (line 13) | function CerticateItem({ certificate, theme }: CertificateItemProps) {
  type CertificatesSection (line 33) | type CertificatesSection = {
  function CertificatesSection (line 37) | function CertificatesSection({

FILE: src/documents/sections/education-section.tsx
  type EducationPlaceItemProps (line 12) | type EducationPlaceItemProps = {
  function EducationPlaceItem (line 17) | function EducationPlaceItem({
  type EducationSectionProps (line 57) | type EducationSectionProps = {
  function EducationSection (line 61) | function EducationSection({

FILE: src/documents/sections/projects-section.tsx
  type ProjectItemProps (line 10) | type ProjectItemProps = {
  function ProjectItem (line 15) | function ProjectItem({ project, theme }: ProjectItemProps) {
  type ProjectsSectionProps (line 41) | type ProjectsSectionProps = {
  function ProjectsSection (line 45) | function ProjectsSection({

FILE: src/documents/sections/publications-section.tsx
  type PublicationItemProps (line 11) | type PublicationItemProps = {
  function PublicationItem (line 16) | function PublicationItem({ publication, theme }: PublicationItemProps) {
  type PublicationsSectionProps (line 36) | type PublicationsSectionProps = {
  function PublicationsSection (line 40) | function PublicationsSection({

FILE: src/documents/sections/skills-section.tsx
  type SkillItemProps (line 8) | type SkillItemProps = {
  function SkillItem (line 12) | function SkillItem({ skill }: SkillItemProps) {
  type SkillsSectionProps (line 22) | type SkillsSectionProps = {
  function SkillsSection (line 26) | function SkillsSection({ skills, theme, ...rest }: SkillsSectionProps) {

FILE: src/documents/sections/volunteer-section.tsx
  type VolunteeringItemProps (line 12) | type VolunteeringItemProps = {
  function VolunteeringItem (line 17) | function VolunteeringItem({
  type VolunteerSectionProps (line 59) | type VolunteerSectionProps = {
  function VolunteerSection (line 63) | function VolunteerSection({ volunteer, theme }: VolunteerSectionProps) {

FILE: src/documents/sections/work-section.tsx
  type JobItemProps (line 12) | type JobItemProps = {
  function JobItem (line 17) | function JobItem({ job, theme }: JobItemProps) {
  type WorkSectionProps (line 62) | type WorkSectionProps = {
  function WorkSection (line 66) | function WorkSection({ work, theme, ...rest }: WorkSectionProps) {

FILE: src/documents/stack.tsx
  type Props (line 5) | type Props = ViewProps & {
  function HStack (line 12) | function HStack({ gap, children, flexWrap, style, ...rest }: Props) {
  function VStack (line 29) | function VStack({ gap, children, flexWrap, style, ...rest }: Props) {

FILE: src/documents/svg-icon.tsx
  type SvgIconProps (line 5) | type SvgIconProps = {
  function SvgIcon (line 12) | function SvgIcon({ size = 24, style, children }: SvgIconProps) {

FILE: src/documents/theme.ts
  type Theme (line 1) | type Theme = {
  function createTheme (line 12) | function createTheme(accentColor = "#2B5DD6", baseFontSize = 10) {

FILE: src/documents/use-has-page-break.ts
  function useHasPageBreak (line 4) | function useHasPageBreak(meta?: Meta | null) {

FILE: src/documents/utils/format-date.ts
  function formatDate (line 1) | function formatDate(stringOrNumber: string | number): string | null {

FILE: src/documents/utils/get-sections-order.ts
  function getSectionsOrder (line 15) | function getSectionsOrder(meta?: Meta | null) {

FILE: src/editing/schema.tsx
  function createHighlightedElements (line 5) | function createHighlightedElements() {

FILE: src/editing/type-highlighter.tsx
  constant IDENT (line 4) | const IDENT = "  ";
  function createTypeHighlighter (line 6) | function createTypeHighlighter() {

FILE: src/editing/yaml-editor.tsx
  type Props (line 10) | type Props = {

FILE: src/icons/download-icon.tsx
  type Props (line 3) | type Props = {
  function DownloadIcon (line 8) | function DownloadIcon({ size = 24, style }: Props) {

FILE: src/icons/folder-icon.tsx
  type Props (line 3) | type Props = {
  function FolderIcon (line 8) | function FolderIcon({ size = 24, style }: Props) {

FILE: src/icons/info-icon.tsx
  type Props (line 3) | type Props = {
  function InfoIcon (line 8) | function InfoIcon({ size, style }: Props) {

FILE: src/icons/pdf-icon.tsx
  type Props (line 3) | type Props = {
  function PDFIcon (line 8) | function PDFIcon({ size, style }: Props) {

FILE: src/icons/plus-icon.tsx
  type Props (line 3) | type Props = {
  function PlusIcon (line 8) | function PlusIcon({ size, style }: Props) {

FILE: src/icons/zoom-in-icon.tsx
  type Props (line 3) | type Props = {
  function ZoomInIcon (line 8) | function ZoomInIcon({ size, style }: Props) {

FILE: src/icons/zoom-out-icon.tsx
  type Props (line 3) | type Props = {
  function ZoomOutIcon (line 8) | function ZoomOutIcon({ size, style }: Props) {

FILE: src/panes-layout.tsx
  type Props (line 5) | type Props = {
  function sashRender (line 11) | function sashRender(_: number, active: boolean) {
  function PanesLayout (line 15) | function PanesLayout({ left, bottom, right }: Props) {

FILE: src/parsing/parse-yaml.ts
  type ParseResult (line 3) | type ParseResult = {
  function parseYAML (line 8) | function parseYAML(yaml: string): ParseResult {

FILE: src/parsing/sample.ts
  constant SAMPLE_YAML (line 1) | const SAMPLE_YAML = `

FILE: src/parsing/use-yaml-parsing.ts
  type Props (line 6) | type Props = {
  constant STORAGE_KEY (line 10) | const STORAGE_KEY = "yaml";
  function useYAMLParsing (line 12) | function useYAMLParsing({ onYAMLParsed }: Props) {

FILE: src/parsing/validate-json.ts
  type ValidationResult (line 8) | type ValidationResult = {
  function validateJSON (line 12) | function validateJSON(json: object): ValidationResult {

FILE: src/parsing/yaml-to-json.ts
  type Result (line 4) | type Result = {
  function resultForParseError (line 12) | function resultForParseError(errorMessage: string): Result {
  function resultFromValidationErrors (line 21) | function resultFromValidationErrors(errorsMessages: string[]): Result {
  function yamlToJSON (line 30) | function yamlToJSON(yaml: string): Result {

FILE: src/persistence/file-management.ts
  function downloadFile (line 1) | function downloadFile(fileName: string, blob: Blob) {
  function readFile (line 14) | function readFile(file: File) {
  function createFileInput (line 33) | function createFileInput(accept: string) {
  function selectFile (line 42) | function selectFile(accept: string) {

FILE: src/persistence/use-yaml-persistence.ts
  type Props (line 4) | type Props = {
  function useYAMLPersistence (line 10) | function useYAMLPersistence({

FILE: src/rendering/debounced-queue.test.ts
  constant ITEM1 (line 5) | const ITEM1 = "1";
  constant ITEM2 (line 6) | const ITEM2 = "2";
  constant ITEM3 (line 7) | const ITEM3 = "3";
  constant DELAY (line 9) | const DELAY = 200;

FILE: src/rendering/debounced-queue.ts
  function createDebouncedQueue (line 3) | function createDebouncedQueue<T>(

FILE: src/rendering/double-buffered.tsx
  type BufferElement (line 3) | type BufferElement = ReactElement | null;
  type Props (line 5) | type Props = {
  function DoubleBuffered (line 9) | function DoubleBuffered({ render }: Props) {

FILE: src/rendering/multi-page-document.tsx
  type Props (line 4) | type Props = {
  function MultiPageDocument (line 10) | function MultiPageDocument({

FILE: src/rendering/pdf.tsx
  type Props (line 7) | type Props = {
  constant PDF (line 12) | const PDF = memo(function ({ blob, scale }: Props) {

FILE: src/rendering/use-render.tsx
  function createDebouncedQueue (line 7) | function createDebouncedQueue<T>(
  function useRender (line 35) | function useRender() {

FILE: src/rendering/use-scale.ts
  type Props (line 4) | type Props = {
  constant EPSILON (line 10) | const EPSILON = 0.00001;
  constant ABS_DELTA (line 12) | const ABS_DELTA = 0.1;
  constant INITIAL_SCALE (line 13) | const INITIAL_SCALE = 1;
  constant STORAGE_KEY (line 14) | const STORAGE_KEY = "scale";
  function useScale (line 16) | function useScale({ minScale, maxScale, absDelta = ABS_DELTA }: Props) {

FILE: src/types.ts
  type Job (line 1) | type Job = {
  type Profile (line 12) | type Profile = {
  type Location (line 16) | type Location = { city?: string; countryCode?: string };
  type Basics (line 18) | type Basics = {
  type Skill (line 29) | type Skill = {
  type Project (line 34) | type Project = {
  type EducationPlace (line 43) | type EducationPlace = {
  type Award (line 53) | type Award = {
  type Certificate (line 60) | type Certificate = {
  type Publication (line 67) | type Publication = {
  type Voluteering (line 75) | type Voluteering = {
  type Meta (line 85) | type Meta = {
  type Resume (line 92) | type Resume = {
  type ResumeSectionName (line 105) | type ResumeSectionName = Exclude<keyof Resume, "meta">;

FILE: src/utils/clamp.ts
  function clamp (line 1) | function clamp(min: number, max: number, value: number) {

FILE: src/utils/deferred.ts
  function createDeferred (line 1) | function createDeferred<T>() {

FILE: src/utils/sleep.ts
  function sleep (line 1) | function sleep(delay: number = 1000) {

FILE: src/utils/use-debounced-effect.ts
  function useDebouncedEffect (line 3) | function useDebouncedEffect(effect: () => void) {
Condensed preview — 114 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (141K chars).
[
  {
    "path": ".eslintrc.cjs",
    "chars": 437,
    "preview": "module.exports = {\n  root: true,\n  env: { browser: true, es2020: true },\n  extends: [\n    \"eslint:recommended\",\n    \"plu"
  },
  {
    "path": ".gitignore",
    "chars": 308,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "README.md",
    "chars": 1289,
    "preview": "<p align=\"center\">\n   <a href=\"https://devresume.app\" target=\"_blank\">\n    <img src=\"screenshot.png\" alt=\"Devices previe"
  },
  {
    "path": "components.json",
    "chars": 323,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": "
  },
  {
    "path": "index.html",
    "chars": 612,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" "
  },
  {
    "path": "package.json",
    "chars": 1519,
    "preview": "{\n  \"name\": \"devresume\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite --"
  },
  {
    "path": "playwright/components/controls.ts",
    "chars": 886,
    "preview": "import { Page, expect } from \"@playwright/test\";\n\nexport function PreviewControls(page: Page) {\n  const zoomIn = () => p"
  },
  {
    "path": "playwright/components/editor.ts",
    "chars": 684,
    "preview": "import { Page, expect } from \"@playwright/test\";\n\nexport function Editor(page: Page) {\n  const self = page.locator(\".cm-"
  },
  {
    "path": "playwright/components/index.ts",
    "chars": 86,
    "preview": "export * from \"./controls\";\nexport * from \"./pdf-document\";\nexport * from \"./editor\";\n"
  },
  {
    "path": "playwright/components/pdf-document.ts",
    "chars": 1076,
    "preview": "import { Page, expect } from \"@playwright/test\";\n\nexport function PDFDocument(page: Page) {\n  // We need to check the da"
  },
  {
    "path": "playwright/controls.spec.ts",
    "chars": 2672,
    "preview": "import { test, expect } from \"@playwright/test\";\nimport { PDFDocument } from \"./components/pdf-document\";\nimport {\n  Edi"
  },
  {
    "path": "playwright/sections.spec.ts",
    "chars": 2576,
    "preview": "import { test } from \"@playwright/test\";\nimport { Editor, PDFDocument } from \"./components\";\nimport {\n  basicsYAML,\n  wo"
  },
  {
    "path": "playwright/yaml.ts",
    "chars": 5108,
    "preview": "export const basicsYAML = `\nbasics:\n  name: Name1 Name2\n  label: Label\n  email: email@test.com\n  phone: (912) 555-4321\n "
  },
  {
    "path": "playwright.config.ts",
    "chars": 1451,
    "preview": "import { defineConfig, devices } from \"@playwright/test\";\n\n/**\n * Read environment variables from file.\n * https://githu"
  },
  {
    "path": "public/cache-sw.js",
    "chars": 788,
    "preview": "self.addEventListener(\"activate\", (event) => {\n  console.log(\"SW: Activate\", event);\n  event.waitUntil(self.clients.clai"
  },
  {
    "path": "public/site.webmanifest",
    "chars": 405,
    "preview": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n       {\n          \"src\": \"/android-chrome-192x192.png\",\n        "
  },
  {
    "path": "src/App.tsx",
    "chars": 2539,
    "preview": "import { RefObject, useCallback, useRef, useState } from \"react\";\nimport { PDF, useRender, useScale } from \"./rendering\""
  },
  {
    "path": "src/app.css",
    "chars": 107,
    "preview": ".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",
    "chars": 130,
    "preview": ".ControlsLayout {\n  display: flex;\n  background-color: var(--color-gray-100);\n  border-bottom: 1px solid var(--color-gra"
  },
  {
    "path": "src/controls/controls-layout.tsx",
    "chars": 460,
    "preview": "import { ReactElement, cloneElement } from \"react\";\nimport \"./controls-layout.css\";\n\ntype Props = {\n  left: ReactElement"
  },
  {
    "path": "src/controls/file-controls.css",
    "chars": 101,
    "preview": ".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",
    "chars": 1029,
    "preview": "import { CSSProperties } from \"react\";\nimport \"./file-controls.css\";\nimport { DownloadIcon, FolderIcon, InfoIcon, PlusIc"
  },
  {
    "path": "src/controls/index.ts",
    "chars": 138,
    "preview": "export * from \"./controls-layout\";\nexport * from \"./preview-controls\";\nexport * from \"./title-controls\";\nexport * from \""
  },
  {
    "path": "src/controls/preview-controls.css",
    "chars": 117,
    "preview": ".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",
    "chars": 1071,
    "preview": "import { CSSProperties } from \"react\";\nimport \"./preview-controls.css\";\nimport { ZoomOutIcon, PDFIcon, ZoomInIcon } from"
  },
  {
    "path": "src/controls/title-controls.css",
    "chars": 427,
    "preview": ".TitleControls {\n  height: 100%;\n  display: flex;\n  justify-content: center;\n  overflow-x: hidden;\n  align-items: center"
  },
  {
    "path": "src/controls/title-controls.tsx",
    "chars": 609,
    "preview": "import { CSSProperties } from \"react\";\nimport \"./title-controls.css\";\n\ntype Props = {\n  title: string;\n  onChange: (valu"
  },
  {
    "path": "src/documents/bar.tsx",
    "chars": 548,
    "preview": "import { View, StyleSheet } from \"@react-pdf/renderer\";\nimport { Theme } from \"./theme\";\nimport { useMemo } from \"react\""
  },
  {
    "path": "src/documents/document.tsx",
    "chars": 4264,
    "preview": "import { Resume } from \"../types\";\nimport { Page, Document, StyleSheet } from \"@react-pdf/renderer\";\nimport { BasicsSect"
  },
  {
    "path": "src/documents/events-section.tsx",
    "chars": 2903,
    "preview": "import { Text, Link } from \"@react-pdf/renderer\";\nimport { Section, SectionProps } from \"./section\";\nimport { HStack, VS"
  },
  {
    "path": "src/documents/fonts.ts",
    "chars": 319,
    "preview": "import { Font } from \"@react-pdf/renderer\";\n\nFont.register({\n  family: \"Roboto\",\n  fonts: [\n    {\n      src: \"RobotoRegu"
  },
  {
    "path": "src/documents/grouped-section.tsx",
    "chars": 1064,
    "preview": "import { Text, View } from \"@react-pdf/renderer\";\nimport { Section } from \"./section\";\nimport { HStack, VStack } from \"."
  },
  {
    "path": "src/documents/icons/github-icon.tsx",
    "chars": 620,
    "preview": "import { SvgIcon, SvgIconProps } from \"../svg-icon\";\nimport { Path } from \"@react-pdf/renderer\";\n\nexport function GitHub"
  },
  {
    "path": "src/documents/icons/globe-icon.tsx",
    "chars": 740,
    "preview": "import { Circle, Line, Path } from \"@react-pdf/renderer\";\nimport { SvgIcon, SvgIconProps } from \"../svg-icon\";\n\nexport f"
  },
  {
    "path": "src/documents/icons/index.ts",
    "chars": 218,
    "preview": "export * from \"./mail-icon\";\nexport * from \"./location-icon\";\nexport * from \"./github-icon\";\nexport * from \"./globe-icon"
  },
  {
    "path": "src/documents/icons/linkedin-icon.tsx",
    "chars": 731,
    "preview": "import { Circle, Path, Rect } from \"@react-pdf/renderer\";\nimport { SvgIcon, SvgIconProps } from \"../svg-icon\";\n\nexport f"
  },
  {
    "path": "src/documents/icons/location-icon.tsx",
    "chars": 462,
    "preview": "import { Path, Circle } from \"@react-pdf/renderer\";\nimport { SvgIcon, SvgIconProps } from \"../svg-icon\";\n\nexport functio"
  },
  {
    "path": "src/documents/icons/mail-icon.tsx",
    "chars": 576,
    "preview": "import { Path, Polyline } from \"@react-pdf/renderer\";\nimport { SvgIconProps } from \"../svg-icon\";\nimport { SvgIcon } fro"
  },
  {
    "path": "src/documents/icons/map-pin-icon.tsx",
    "chars": 529,
    "preview": "import { Circle, Path } from \"@react-pdf/renderer\";\nimport { SvgIcon, SvgIconProps } from \"../svg-icon\";\n\nexport functio"
  },
  {
    "path": "src/documents/icons/phone-icon.tsx",
    "chars": 613,
    "preview": "import { SvgIcon, SvgIconProps } from \"../svg-icon\";\nimport { Path } from \"@react-pdf/renderer\";\n\nexport function PhoneI"
  },
  {
    "path": "src/documents/index.ts",
    "chars": 53,
    "preview": "export * from \"./document\";\nexport * from \"./fonts\";\n"
  },
  {
    "path": "src/documents/rich-text.tsx",
    "chars": 1247,
    "preview": "import { Text, Link } from \"@react-pdf/renderer\";\nimport Markdown from \"markdown-to-jsx\";\nimport { ReactNode } from \"rea"
  },
  {
    "path": "src/documents/section.tsx",
    "chars": 853,
    "preview": "import { ReactNode, useMemo } from \"react\";\nimport { Text, View, StyleSheet } from \"@react-pdf/renderer\";\nimport { Theme"
  },
  {
    "path": "src/documents/sections/awards-section.tsx",
    "chars": 1081,
    "preview": "import { Award } from \"../../types\";\nimport { Text } from \"@react-pdf/renderer\";\nimport {\n  EventItem,\n  EventsSection,\n"
  },
  {
    "path": "src/documents/sections/basics-section/contacts.tsx",
    "chars": 1901,
    "preview": "import { InfoItem } from \"./info-item\";\nimport {\n  GitHubIcon,\n  GlobeIcon,\n  LinkedInIcon,\n  MailIcon,\n  PhoneIcon,\n} f"
  },
  {
    "path": "src/documents/sections/basics-section/index.tsx",
    "chars": 1431,
    "preview": "import { Text, View, StyleSheet } from \"@react-pdf/renderer\";\nimport { Section, SectionProps } from \"../../section\";\nimp"
  },
  {
    "path": "src/documents/sections/basics-section/info-item.tsx",
    "chars": 991,
    "preview": "import { Text, Link, StyleSheet } from \"@react-pdf/renderer\";\nimport { ReactElement, cloneElement, useMemo } from \"react"
  },
  {
    "path": "src/documents/sections/basics-section/location-info-item.tsx",
    "chars": 531,
    "preview": "import { Location } from \"../../../types\";\nimport { MapPinIcon } from \"../../icons\";\nimport { Theme } from \"../../theme\""
  },
  {
    "path": "src/documents/sections/certificates-section.tsx",
    "chars": 1291,
    "preview": "import { Certificate } from \"../../types\";\nimport { Text } from \"@react-pdf/renderer\";\nimport { EventItem, EventsSection"
  },
  {
    "path": "src/documents/sections/education-section.tsx",
    "chars": 1848,
    "preview": "import { EducationPlace } from \"../../types\";\nimport { Link } from \"@react-pdf/renderer\";\nimport {\n  EventHighlightItem,"
  },
  {
    "path": "src/documents/sections/index.ts",
    "chars": 324,
    "preview": "export * from \"./basics-section\";\nexport * from \"./work-section\";\nexport * from \"./education-section\";\nexport * from \"./"
  },
  {
    "path": "src/documents/sections/projects-section.tsx",
    "chars": 1289,
    "preview": "import { Project } from \"../../types\";\nimport {\n  EventHighlightItem,\n  EventItem,\n  EventsSection,\n  EventsSectionProps"
  },
  {
    "path": "src/documents/sections/publications-section.tsx",
    "chars": 1336,
    "preview": "import { Publication } from \"../../types\";\nimport { Text } from \"@react-pdf/renderer\";\nimport {\n  EventItem,\n  EventsSec"
  },
  {
    "path": "src/documents/sections/skills-section.tsx",
    "chars": 794,
    "preview": "import { Skill } from \"../../types\";\nimport {\n  GroupItem,\n  GroupedSection,\n  GroupedSectionProps,\n} from \"../grouped-s"
  },
  {
    "path": "src/documents/sections/volunteer-section.tsx",
    "chars": 1835,
    "preview": "import { Voluteering } from \"../../types\";\nimport { Link } from \"@react-pdf/renderer\";\nimport {\n  EventHighlightItem,\n  "
  },
  {
    "path": "src/documents/sections/work-section.tsx",
    "chars": 1657,
    "preview": "import { Job } from \"../../types\";\nimport { Link, Text } from \"@react-pdf/renderer\";\nimport {\n  EventHighlightItem,\n  Ev"
  },
  {
    "path": "src/documents/stack.tsx",
    "chars": 858,
    "preview": "import { ReactNode } from \"react\";\nimport { View } from \"@react-pdf/renderer\";\nimport { Style, ViewProps } from \"@react-"
  },
  {
    "path": "src/documents/svg-icon.tsx",
    "chars": 419,
    "preview": "import { Svg } from \"@react-pdf/renderer\";\nimport { Style } from \"@react-pdf/types\";\nimport { ReactNode } from \"react\";\n"
  },
  {
    "path": "src/documents/theme.ts",
    "chars": 729,
    "preview": "export type Theme = {\n  lineHeight: number;\n  space: Array<number>;\n  fontSize: Array<number>;\n  color: {\n    text: stri"
  },
  {
    "path": "src/documents/use-has-page-break.ts",
    "chars": 597,
    "preview": "import { useCallback, useMemo } from \"react\";\nimport { Meta, ResumeSectionName } from \"../types\";\n\nexport function useHa"
  },
  {
    "path": "src/documents/utils/format-date.test.ts",
    "chars": 452,
    "preview": "import { expect, test } from \"vitest\";\nimport { formatDate } from \".\";\n\nconst tests = [\n  [2021, \"2021\"],\n  [\"2021\", \"20"
  },
  {
    "path": "src/documents/utils/format-date.ts",
    "chars": 464,
    "preview": "export function formatDate(stringOrNumber: string | number): string | null {\n  const dateString = stringOrNumber.toStrin"
  },
  {
    "path": "src/documents/utils/get-sections-order.test.ts",
    "chars": 1078,
    "preview": "import { expect, test } from \"vitest\";\nimport { defaultSectionsOrder, getSectionsOrder } from \"./get-sections-order\";\nim"
  },
  {
    "path": "src/documents/utils/get-sections-order.ts",
    "chars": 672,
    "preview": "import { Meta, ResumeSectionName } from \"../../types\";\n\nexport const defaultSectionsOrder: ResumeSectionName[] = [\n  \"ba"
  },
  {
    "path": "src/documents/utils/index.ts",
    "chars": 69,
    "preview": "export * from \"./format-date\";\nexport * from \"./get-sections-order\";\n"
  },
  {
    "path": "src/editing/index.ts",
    "chars": 57,
    "preview": "export * from \"./yaml-editor\";\nexport * from \"./schema\";\n"
  },
  {
    "path": "src/editing/schema.css",
    "chars": 515,
    "preview": ".Schema {\n  background-color: var(--color-gray-100);\n  border-top: 1px solid var(--color-gray-400);\n  border-right: 1px "
  },
  {
    "path": "src/editing/schema.tsx",
    "chars": 4629,
    "preview": "import { createTypeHighlighter } from \"./type-highlighter\";\nimport \"./schema.css\";\nimport { memo, useMemo } from \"react\""
  },
  {
    "path": "src/editing/type-highlighter.tsx",
    "chars": 3932,
    "preview": "import { Fragment, ReactElement } from \"react\";\n\n// eslint-disable-next-line react-refresh/only-export-components\nconst "
  },
  {
    "path": "src/editing/yaml-editor.css",
    "chars": 136,
    "preview": ".YAMLEditor {\n  font-size: var(--font-size-1);\n  overflow-y: hidden;\n  height: 100%;\n  border-right: 1px solid var(--col"
  },
  {
    "path": "src/editing/yaml-editor.tsx",
    "chars": 864,
    "preview": "import CodeMirror, { ReactCodeMirrorRef } from \"@uiw/react-codemirror\";\nimport * as yamlMode from \"@codemirror/legacy-mo"
  },
  {
    "path": "src/icons/download-icon.tsx",
    "chars": 629,
    "preview": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function Dow"
  },
  {
    "path": "src/icons/folder-icon.tsx",
    "chars": 554,
    "preview": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function Fol"
  },
  {
    "path": "src/icons/index.ts",
    "chars": 215,
    "preview": "export * from \"./folder-icon\";\nexport * from \"./download-icon\";\nexport * from \"./pdf-icon\";\nexport * from \"./zoom-in-ico"
  },
  {
    "path": "src/icons/info-icon.tsx",
    "chars": 597,
    "preview": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function Inf"
  },
  {
    "path": "src/icons/pdf-icon.tsx",
    "chars": 736,
    "preview": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function PDF"
  },
  {
    "path": "src/icons/plus-icon.tsx",
    "chars": 547,
    "preview": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function Plu"
  },
  {
    "path": "src/icons/zoom-in-icon.tsx",
    "chars": 656,
    "preview": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function Zoo"
  },
  {
    "path": "src/icons/zoom-out-icon.tsx",
    "chars": 607,
    "preview": "import { CSSProperties } from \"react\";\n\ntype Props = {\n  size?: number;\n  style?: CSSProperties;\n};\n\nexport function Zoo"
  },
  {
    "path": "src/index.css",
    "chars": 1893,
    "preview": "body {\n  margin: 0;\n}\nbody,\nbutton,\ninput {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Rob"
  },
  {
    "path": "src/main.tsx",
    "chars": 864,
    "preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { App } from \"./App\";\nimport \"./index.css\";\ni"
  },
  {
    "path": "src/panes-layout.tsx",
    "chars": 1105,
    "preview": "import { ReactElement, useState } from \"react\";\nimport { Pane, SashContent } from \"split-pane-react\";\nimport SplitPane f"
  },
  {
    "path": "src/parsing/index.ts",
    "chars": 36,
    "preview": "export * from \"./use-yaml-parsing\";\n"
  },
  {
    "path": "src/parsing/parse-yaml.ts",
    "chars": 317,
    "preview": "import YAML from \"yaml\";\n\ntype ParseResult = {\n  json?: object;\n  errorMessage?: string;\n};\n\nexport function parseYAML(y"
  },
  {
    "path": "src/parsing/resume-schema.json",
    "chars": 14973,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"additionalProperties\": false,\n  \"definitions\": {\n    \"iso86"
  },
  {
    "path": "src/parsing/sample.ts",
    "chars": 3181,
    "preview": "export const SAMPLE_YAML = `\n# Demo resume adapted from https://github.com/jsonresume/resume-schema/blob/master/sample.r"
  },
  {
    "path": "src/parsing/use-yaml-parsing.test.tsx",
    "chars": 2127,
    "preview": "import { describe, expect, test } from \"vitest\";\nimport { act, renderHook } from \"@testing-library/react\";\nimport { useY"
  },
  {
    "path": "src/parsing/use-yaml-parsing.ts",
    "chars": 977,
    "preview": "import { useCallback, useState } from \"react\";\nimport { useDebouncedEffect } from \"../utils\";\nimport { yamlToJSON } from"
  },
  {
    "path": "src/parsing/validate-json.ts",
    "chars": 507,
    "preview": "// Validaiton\n\nimport Validator from \"z-schema\";\nimport resumeSchema from \"./resume-schema.json\";\n\nconst validator = new"
  },
  {
    "path": "src/parsing/yaml-to-json.ts",
    "chars": 1026,
    "preview": "import { parseYAML } from \"./parse-yaml\";\nimport { validateJSON } from \"./validate-json\";\n\ntype Result = {\n  json?: obje"
  },
  {
    "path": "src/persistence/file-management.ts",
    "chars": 1314,
    "preview": "export function downloadFile(fileName: string, blob: Blob) {\n  const a: HTMLAnchorElement = document.createElement(\"a\");"
  },
  {
    "path": "src/persistence/index.ts",
    "chars": 75,
    "preview": "export * from \"./use-yaml-persistence\";\nexport * from \"./file-management\";\n"
  },
  {
    "path": "src/persistence/use-yaml-persistence.test.ts",
    "chars": 1716,
    "preview": "import { test, expect, describe, afterEach } from \"vitest\";\nimport { renderHook } from \"@testing-library/react\";\nimport "
  },
  {
    "path": "src/persistence/use-yaml-persistence.ts",
    "chars": 937,
    "preview": "import { useCallback } from \"react\";\nimport { downloadFile, readFile, selectFile } from \"./file-management\";\n\ntype Props"
  },
  {
    "path": "src/rendering/debounced-queue.test.ts",
    "chars": 1401,
    "preview": "import { expect, test } from \"vitest\";\nimport { createDebouncedQueue } from \"./debounced-queue\";\nimport { createDeferred"
  },
  {
    "path": "src/rendering/debounced-queue.ts",
    "chars": 510,
    "preview": "import { sleep } from \"../utils\";\n\nexport function createDebouncedQueue<T>(\n  callback: (items: Array<T>) => Promise<voi"
  },
  {
    "path": "src/rendering/double-buffered.tsx",
    "chars": 1327,
    "preview": "import { ReactElement, cloneElement, useEffect, useRef, useState } from \"react\";\n\ntype BufferElement = ReactElement | nu"
  },
  {
    "path": "src/rendering/index.ts",
    "chars": 82,
    "preview": "export * from \"./pdf\";\nexport * from \"./use-render\";\nexport * from \"./use-scale\";\n"
  },
  {
    "path": "src/rendering/multi-page-document.tsx",
    "chars": 1724,
    "preview": "import { HTMLAttributes, useCallback, useMemo, useRef, useState } from \"react\";\nimport { Document, Page } from \"react-pd"
  },
  {
    "path": "src/rendering/pdf.css",
    "chars": 103,
    "preview": ".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",
    "chars": 1472,
    "preview": "import { memo, useCallback, useEffect, useRef } from \"react\";\nimport { DoubleBuffered } from \"./double-buffered\";\nimport"
  },
  {
    "path": "src/rendering/use-render.tsx",
    "chars": 1505,
    "preview": "import { useCallback, useMemo, useRef, useState } from \"react\";\nimport { pdf } from \"@react-pdf/renderer\";\nimport { Resu"
  },
  {
    "path": "src/rendering/use-scale.test.ts",
    "chars": 2670,
    "preview": "import { expect, test } from \"vitest\";\nimport { renderHook } from \"@testing-library/react\";\nimport { useScale, STORAGE_K"
  },
  {
    "path": "src/rendering/use-scale.ts",
    "chars": 1198,
    "preview": "import { useCallback, useState } from \"react\";\nimport { clamp } from \"../utils/clamp\";\n\ntype Props = {\n  absDelta?: numb"
  },
  {
    "path": "src/types.ts",
    "chars": 2148,
    "preview": "export type Job = {\n  name?: string;\n  location?: string;\n  position?: string;\n  url?: string;\n  summary?: string;\n  sta"
  },
  {
    "path": "src/utils/clamp.ts",
    "chars": 113,
    "preview": "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",
    "chars": 345,
    "preview": "export function createDeferred<T>() {\n  let resolve: (value: T) => void = () => {};\n  let reject: (reason?: unknown) => "
  },
  {
    "path": "src/utils/index.ts",
    "chars": 118,
    "preview": "export * from \"./clamp\";\nexport * from \"./use-debounced-effect\";\nexport * from \"./sleep\";\nexport * from \"./deferred\";\n"
  },
  {
    "path": "src/utils/sleep.ts",
    "chars": 111,
    "preview": "export function sleep(delay: number = 1000) {\n  return new Promise((resolve) => setTimeout(resolve, delay));\n}\n"
  },
  {
    "path": "src/utils/use-debounced-effect.ts",
    "chars": 238,
    "preview": "import { useEffect } from \"react\";\n\nexport function useDebouncedEffect(effect: () => void) {\n  useEffect(() => {\n    con"
  },
  {
    "path": "src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "vite.config.ts",
    "chars": 296,
    "preview": "/// <reference types=\"vitest\" />\n\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n// htt"
  }
]

About this extraction

This page contains the full source code of the vangelov/devresume GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 114 files (123.3 KB), approximately 35.7k tokens, and a symbol index with 175 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!