Skip to content

Commit 459598d

Browse files
committed
Add customizable profile layout system
1 parent cde144d commit 459598d

10 files changed

Lines changed: 2040 additions & 222 deletions

File tree

README.md

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,30 @@ A browser-based editor for creating a personal GitHub profile README from one re
44

55
The built-in TFQ0 design remains the canonical example, but users no longer need to edit eight SVG files by hand.
66

7-
## Design and media customization
7+
## Templates, layout, and media customization
88

9-
Configuration v2 adds three focused customization systems:
9+
Configuration v3 provides seven templates: Quality Control, Classic Terminal, Retro Arcade, Anime HUD, Bento Grid, Signal Poster, and Custom Canvas. The templates use generic, original interface elements and include no franchise characters, logos, or copied artwork.
1010

11-
- Four ready-made visual presets: Quality Control, Classic Terminal, Retro Arcade, and Anime HUD. They use generic interface elements and include no franchise characters, logos, or copied artwork.
12-
- Curated font presets that map to controlled, SVG-safe font stacks rather than accepting arbitrary CSS.
13-
- Structured remote media entries for images and GIFs referenced by HTTPS URL, with accessibility and attribution information kept alongside each entry.
11+
The templates are more than palette swaps: they provide structurally distinct compositions that adapt independently to desktop and mobile output. Custom Canvas uses the same validated rendering system and offers constrained layout controls rather than unrestricted canvas editing.
1412

15-
Applying a visual preset changes presentation without replacing a user's profile text, repositories, links, or media. Loading a complete sample is a separate, confirmed action. Valid v1 configurations migrate automatically when imported or restored from browser storage.
13+
The customization contract includes:
14+
15+
- Responsive composition, alignment, spacing, and placement choices within safe layout bounds.
16+
- Two to six workflow steps, with an allowlisted safe shape selected for each step.
17+
- Decorative shapes described through a bounded SVG-safe DSL. The renderer converts validated shape data into SVG; users cannot insert arbitrary SVG elements or path commands.
18+
- Curated font presets that map to controlled, SVG-safe system font stacks.
19+
- Structured remote media entries for images and GIFs referenced by HTTPS URL, with accessibility and attribution information kept alongside each entry. Media remains separate README content and is not inserted into the generated hero canvas.
20+
21+
Applying a template changes its visual system and layout defaults without replacing the user's profile text, repositories, links, sections, or media. Loading a complete sample is a separate, confirmed action that replaces profile content. Valid v1 and v2 configurations migrate automatically when imported or restored from browser storage.
22+
23+
The editor does not accept arbitrary raw CSS, SVG markup, SVG paths, or remote fonts. Decorative elements are code-generated from allowlisted primitives and bounded numeric values.
1624

1725
## What it includes
1826

1927
- Live desktop and mobile preview in dark and light themes.
2028
- Animated and reduced-motion/static SVG variants.
21-
- Design, font, profile, hero, project, skill, link, structured media, palette, section, Markdown, and footer editing.
29+
- Seven structurally distinct responsive templates plus constrained custom layout controls.
30+
- Design, font, profile, hero, workflow-step shape, decorative shape, project, skill, link, structured media, palette, section, Markdown, and footer editing.
2231
- Optional public GitHub repository import with no login or token.
2332
- Local autosave of the last valid configuration.
2433
- Versioned `profile.config.json` import/export.
@@ -29,13 +38,14 @@ Applying a visual preset changes presentation without replacing a user's profile
2938

3039
## Use the studio
3140

32-
1. Open the app, choose a design, then select **Start a blank profile** or customize the TFQ0 example.
33-
2. Work through Profile, Hero, Projects, Skills, Links, Media, Colors, and Sections.
34-
3. Check the desktop/mobile, dark/light, and animated/static previews.
35-
4. Resolve validation errors and review any design warnings in Export.
36-
5. To keep only the image, choose its desktop/mobile, dark/light, and animated/static options, then select **Download SVG**.
37-
6. To publish the complete profile, download the ZIP instead.
38-
7. Upload its `README.md` and `assets/` directory to the public GitHub repository whose name exactly matches your username.
41+
1. Open the app and choose one of the seven templates, then adjust its constrained layout controls if desired.
42+
2. Select **Start a blank profile**, customize the TFQ0 example, or explicitly load another complete content sample.
43+
3. Work through Profile, Hero, workflow steps and shapes, Projects, Skills, Links, Media, Colors, and Sections.
44+
4. Check the desktop/mobile, dark/light, and animated/static previews. Responsive compositions can adapt between desktop and mobile rather than preserving identical coordinates.
45+
5. Resolve validation errors and review any design warnings in Export.
46+
6. To keep only the image, choose its desktop/mobile, dark/light, and animated/static options, then select **Download SVG**.
47+
7. To publish the complete profile, download the ZIP instead.
48+
8. Upload its `README.md` and `assets/` directory to the public GitHub repository whose name exactly matches your username.
3949

4050
Keep the exported `profile.config.json`; importing it later restores an editable profile instead of requiring manual SVG changes.
4151

@@ -62,7 +72,8 @@ The production site is written to `dist/`. Vite uses relative asset URLs, so the
6272

6373
```text
6474
src/
65-
├── domain/profile.ts # strict versioned config and template metadata
75+
├── domain/profile.ts # strict v3 config plus v1/v2 migration
76+
├── domain/presets.ts # trusted template defaults and visual presets
6677
├── generator/
6778
│ ├── svg.ts # pure eight-variant SVG renderer
6879
│ ├── readme.ts # GitHub README renderer
@@ -74,14 +85,15 @@ src/
7485
└── App.tsx # editor state and workflows
7586
```
7687

77-
`ProfileConfig` is the single source of truth. The visible preview, README source, SVG files, saved config, and ZIP are all produced from the same validated object. Imported GitHub data becomes an editable snapshot; it is never a hidden dependency of later exports.
88+
`ProfileConfig` is the single source of truth. The visible preview, README source, SVG files, saved config, and ZIP are all produced from the same validated object. Template, responsive layout, workflow-shape, and decorative-shape values are resolved through trusted renderer registries. Imported GitHub data becomes an editable snapshot; it is never a hidden dependency of later exports.
7889

7990
## Privacy and output safety
8091

8192
- The editor has no backend and requests no GitHub token.
8293
- Drafts and the short-lived public repository cache stay in browser storage.
8394
- Only public repository metadata is fetched directly from GitHub's public API.
8495
- Generated SVGs contain no scripts, event handlers, remote fonts, external images, tracking, or live CI claims.
96+
- Layout and decorative SVG output is generated only from validated, bounded controls and allowlisted shape primitives; raw CSS, SVG markup, and path data are not accepted.
8597
- User-selected media is referenced by HTTPS URL in the README rather than copied into the ZIP or embedded in the generated hero SVG.
8698
- Previewing or viewing remote media may send an ordinary request to its third-party host. Remote content can disappear or change independently of the Studio.
8799
- Static fallbacks are generated for visitors who prefer reduced motion.

SETUP.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,27 @@ npm run build
2121

2222
These checks cover TypeScript, strict config parsing, all eight SVG variants, reduced-motion output, README-to-asset references, deterministic ZIP contents, unsafe input handling, public GitHub import errors, cache validation, and the WebMCP contract.
2323

24-
For the v2 configuration, verification also covers every design and font preset, migration of valid v1 drafts, structured remote-media validation and attribution, rejection of unsafe media URLs, and preservation of user content when applying a visual preset.
24+
The v3 verification matrix must also cover:
2525

26-
For a release, also inspect representative long content in desktop/mobile and dark/light previews. Design warnings are advisory because fixed SVG artwork cannot measure browser font metrics during generation.
26+
- Quality Control, Classic Terminal, Retro Arcade, Anime HUD, Bento Grid, Signal Poster, and Custom Canvas.
27+
- Every supported responsive composition in desktop and mobile output, including constrained alignment, spacing, and placement boundary values.
28+
- Two-step and six-step workflows, every allowlisted per-step shape, ordering, and text-fit limits.
29+
- Decorative shapes at their count and numeric boundaries, plus rejection of unknown primitives, excessive values, raw SVG, arbitrary path data, and CSS injection.
30+
- Every curated font preset and rejection of arbitrary font-family values or remote font references.
31+
- Migration of valid v1 and v2 drafts to v3, with user content and existing visual choices preserved.
32+
- Structured remote-media validation and attribution, rejection of unsafe media URLs, and confirmation that media remains separate HTTPS README content rather than hero SVG content.
33+
- Preservation of profile text, repositories, links, sections, and media when applying a template. Loading a complete sample must remain a separate confirmed replacement action.
34+
35+
For a release, inspect all seven templates with representative and maximum-length content in desktop/mobile, dark/light, and animated/static previews. Confirm that compositions are visibly distinct, remain within the SVG viewBox, and adapt safely between desktop and mobile. Design warnings are advisory because fixed SVG artwork cannot measure browser font metrics during generation.
36+
37+
Exercise the constrained custom layout controls at minimum and maximum values, then verify that the visible preview, downloaded current SVG, and matching ZIP asset render the same configuration. Check workflows with two and six steps and mix every safe step shape. Decorative-shape tests must confirm that generated primitives remain bounded and cannot obscure required text or controls.
2738

2839
The preview toolbar can download its currently selected SVG by itself. Use the Export panel's complete ZIP when publishing the full README and all responsive theme variants.
2940

3041
When changing the editor layout, inspect 1440 px, 1180 px, 900 px, 620 px, and 320 px viewport widths. Confirm that navigation and export actions remain reachable, keyboard focus remains visible, remote media stays within the preview, and no horizontal page overflow is introduced.
3142

43+
Custom Canvas is constrained despite its name. Do not add arbitrary raw CSS, SVG markup, SVG path input, or remote fonts. Layouts, workflow shapes, and decorative shapes must be selected or composed through validated controls and code-generated renderer primitives.
44+
3245
## Production output
3346

3447
`npm run build` creates the static site in `dist/`. The build uses relative asset paths and can be hosted at a domain root or a project subpath.

src/App.test.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,64 @@ describe("Profile Studio workspace", () => {
7474
"preview-heading",
7575
);
7676
});
77+
78+
it("applies seven content-safe presets, customizes layout, and grows a six-step workflow", () => {
79+
const presetCards = Array.from(
80+
container.querySelectorAll<HTMLButtonElement>(".preset-card"),
81+
);
82+
expect(presetCards).toHaveLength(7);
83+
expect(presetCards.map((card) => card.querySelector("strong")?.textContent)).toEqual([
84+
"Quality Control",
85+
"Classic Terminal",
86+
"Retro Arcade",
87+
"Anime HUD",
88+
"Bento Grid",
89+
"Signal Poster",
90+
"Custom Canvas",
91+
]);
92+
expect(container.querySelector(".generated-hero")?.textContent).toContain("TALAL ALQAHS");
93+
94+
const bento = presetCards.find((card) => card.textContent?.includes("Bento Grid"))!;
95+
act(() => bento.click());
96+
97+
expect(bento.getAttribute("aria-pressed")).toBe("true");
98+
expect(container.querySelector(".generated-hero")?.textContent).toContain("TALAL ALQAHS");
99+
expect(container.querySelector(".generated-hero svg")?.getAttribute("data-composition")).toBe(
100+
"bento",
101+
);
102+
103+
const compositionLabel = Array.from(container.querySelectorAll("label")).find(
104+
(label) => label.querySelector(".field-label-row > span")?.textContent === "Composition",
105+
)!;
106+
const composition = compositionLabel.querySelector("select")!;
107+
act(() => {
108+
composition.value = "poster";
109+
composition.dispatchEvent(new Event("change", { bubbles: true }));
110+
});
111+
112+
expect(composition.value).toBe("poster");
113+
expect(bento.getAttribute("aria-pressed")).toBe("false");
114+
expect(container.querySelector(".generated-hero svg")?.getAttribute("data-composition")).toBe(
115+
"poster",
116+
);
117+
118+
act(() => container.querySelector<HTMLButtonElement>("#step-hero")!.click());
119+
const workflowCard = Array.from(container.querySelectorAll<HTMLElement>(".editor-card")).find(
120+
(card) => card.querySelector("h3")?.textContent === "Workflow",
121+
)!;
122+
for (let count = 4; count < 6; count += 1) {
123+
const addStep = Array.from(workflowCard.querySelectorAll<HTMLButtonElement>("button")).find(
124+
(button) => button.textContent?.trim() === "Add step",
125+
)!;
126+
act(() => addStep.click());
127+
}
128+
129+
expect(workflowCard.querySelector(".list-heading span")?.textContent).toBe("6/6");
130+
expect(workflowCard.querySelectorAll("input[maxlength='12']")).toHaveLength(6);
131+
expect(
132+
Array.from(workflowCard.querySelectorAll("button")).some(
133+
(button) => button.textContent?.trim() === "Add step",
134+
),
135+
).toBe(false);
136+
});
77137
});

0 commit comments

Comments
 (0)