feat:icon colors (#2928)

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Ruzenie
2026-04-27 21:12:55 +08:00
committed by GitHub
parent 6a8dd480cb
commit 118004b3d3
84 changed files with 1992 additions and 736 deletions
+75 -9
View File
@@ -1,18 +1,43 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { type ColorResult, hsvaToRgbaString, rgbaStringToHsva } from "@uiw/color-convert";
import ReactColorColorful from "@uiw/react-color-colorful";
import { useMemo } from "react";
import { useControlledState } from "@/hooks/use-controlled-state";
import { cn } from "@/utils/style";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Separator } from "../ui/separator";
const presetColors = [
"rgba(0, 0, 0, 1)",
"rgba(231, 0, 11, 1)",
"rgba(245, 73, 0, 1)",
"rgba(225, 113, 0, 1)",
"rgba(208, 135, 0, 1)",
"rgba(94, 165, 0, 1)",
"rgba(0, 166, 62, 1)",
"rgba(0, 153, 102, 1)",
"rgba(0, 146, 184, 1)",
"rgba(0, 132, 209, 1)",
"rgba(21, 93, 252, 1)",
"rgba(79, 57, 246, 1)",
"rgba(127, 34, 254, 1)",
"rgba(200, 0, 222, 1)",
"rgba(230, 0, 118, 1)",
"rgba(69, 85, 108, 1)",
] as const;
type ColorPickerProps = {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
trigger?: React.ReactNode;
children?: React.ReactNode;
};
export function ColorPicker({ value, defaultValue, onChange }: ColorPickerProps) {
export function ColorPicker({ value, defaultValue, onChange, trigger, children }: ColorPickerProps) {
const [currentValue, setCurrentValue] = useControlledState<string>({
value,
defaultValue,
@@ -28,15 +53,56 @@ export function ColorPicker({ value, defaultValue, onChange }: ColorPickerProps)
return (
<Popover>
<PopoverTrigger>
<div
className="size-6 shrink-0 cursor-pointer rounded-full border border-foreground transition-opacity hover:opacity-60"
style={{ backgroundColor: currentValue }}
/>
</PopoverTrigger>
{trigger ?? (
<PopoverTrigger>
<div
className="size-6 shrink-0 cursor-pointer rounded-full border border-foreground/60 transition-all hover:scale-105 focus-visible:outline-hidden"
style={{ backgroundColor: currentValue }}
/>
</PopoverTrigger>
)}
<PopoverContent className="max-w-fit rounded-md p-2">
<ReactColorColorful color={color} onChange={onColorChange} />
<PopoverContent align="start" className="min-w-xs">
{children && (
<>
{children}
<Separator />
</>
)}
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-muted-foreground">
<Trans>Presets</Trans>
</span>
<div className="grid grid-cols-8 gap-3 rounded bg-muted p-3">
{presetColors.map((color) => (
<button
key={color}
type="button"
title={color}
style={{ backgroundColor: color }}
aria-label={t`Use color ${color}`}
aria-pressed={currentValue === color}
onClick={() => setCurrentValue(color)}
className={cn(
"size-5 shrink-0 cursor-pointer rounded-full transition-all hover:scale-105 focus-visible:outline-hidden",
currentValue === color && "border border-foreground/60",
)}
/>
))}
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-muted-foreground">
<Trans>Custom</Trans>
</span>
<div className="rounded bg-muted p-3 *:w-full! [&_.w-color-alpha]:mt-4! [&_.w-color-alpha]:h-4! [&_.w-color-alpha>div]:rounded-full! [&_.w-color-hue]:mt-4! [&_.w-color-hue]:h-4! [&_.w-color-hue]:rounded-full! [&_.w-color-saturation]:h-36! [&_.w-color-saturation]:rounded-[calc(var(--radius-lg)-0.25rem)]!">
<ReactColorColorful color={color} onChange={onColorChange} />
</div>
</div>
</PopoverContent>
</Popover>
);
@@ -176,26 +176,3 @@
text-underline-offset: 0.15rem;
}
}
.color_picker {
overflow: hidden;
border-radius: var(--radius-lg);
background: var(--color-muted);
padding: 0.5rem;
> :global(*) {
width: 100% !important;
}
:global(.w-color-saturation) {
height: 8.5rem !important;
border-radius: calc(var(--radius-lg) - 0.25rem) !important;
}
:global(.w-color-hue),
:global(.w-color-alpha) {
height: 0.75rem !important;
margin-top: 0.5rem !important;
border-radius: 9999px !important;
}
}
+53 -112
View File
@@ -52,8 +52,6 @@ import {
useEditorState,
} from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { type ColorResult, hsvaToRgbaString, rgbaStringToHsva } from "@uiw/color-convert";
import ReactColorColorful from "@uiw/react-color-colorful";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { match } from "ts-pattern";
@@ -69,37 +67,18 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover";
import { usePrompt } from "@/hooks/use-prompt";
import { isRTL } from "@/utils/locale";
import { sanitizeHtml } from "@/utils/sanitize";
import { cn } from "@/utils/style";
import { Toggle } from "../ui/toggle";
import { ColorPicker } from "./color-picker";
import styles from "./rich-input.module.css";
const defaultTextColor = "rgba(0, 0, 0, 1)";
const richTextColorOptions = [
"rgba(0, 0, 0, 1)",
"rgba(231, 0, 11, 1)",
"rgba(245, 73, 0, 1)",
"rgba(225, 113, 0, 1)",
"rgba(208, 135, 0, 1)",
"rgba(94, 165, 0, 1)",
"rgba(0, 166, 62, 1)",
"rgba(0, 153, 102, 1)",
"rgba(0, 146, 184, 1)",
"rgba(0, 132, 209, 1)",
"rgba(21, 93, 252, 1)",
"rgba(79, 57, 246, 1)",
"rgba(127, 34, 254, 1)",
"rgba(200, 0, 222, 1)",
"rgba(230, 0, 118, 1)",
"rgba(69, 85, 108, 1)",
] as const;
const extensions = [
StarterKit.configure({
heading: {
@@ -461,103 +440,65 @@ function EditorToolbar({ editor, isFullscreen }: { editor: Editor; isFullscreen:
<HighlighterCircleIcon className="size-3.5" />
</Toggle>
<Popover>
<PopoverTrigger
render={
<Button
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className={cn("rounded-none px-2", state.textColor && "bg-muted text-foreground")}
title={t`Text Color`}
disabled={!state.canTextColor}
>
<span className="flex flex-col items-center leading-none">
<span className="text-xs font-semibold">A</span>
<span
className="mt-0.5 h-0.5 w-3 rounded-full"
style={{ backgroundColor: state.textColor ?? "currentColor" }}
/>
</span>
</Button>
}
/>
<PopoverContent sideOffset={8} className="w-80 gap-3 rounded-xl p-3 shadow-xl">
<PopoverHeader className="flex-row items-start justify-between gap-2">
<div className="flex items-center gap-2.5">
<span
className="grid size-9 place-items-center rounded-lg border border-border bg-muted/60 text-sm font-semibold shadow-xs"
style={{ color: state.textColor ?? "currentColor" }}
<ColorPicker
defaultValue={defaultTextColor}
value={state.textColor ?? undefined}
onChange={state.setTextColor}
trigger={
<PopoverTrigger
render={
<Button
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className={cn("rounded-none px-2", state.textColor && "bg-muted text-foreground")}
title={t`Text Color`}
disabled={!state.canTextColor}
>
A
</span>
<div className="flex flex-col gap-0.5">
<PopoverTitle>
<Trans>Text Color</Trans>
</PopoverTitle>
<span className="text-xs text-muted-foreground">
<Trans comment="Preset or custom shade refer to the color picker">
Choose a preset or custom shade.
</Trans>
<span className="flex flex-col items-center leading-none">
<span className="text-xs font-semibold">A</span>
<span
className="mt-0.5 h-0.5 w-3 rounded-full"
style={{ backgroundColor: state.textColor ?? "currentColor" }}
/>
</span>
</div>
</div>
<Button
size="xs"
variant="ghost"
className="shrink-0"
onClick={state.unsetTextColor}
disabled={!state.textColor}
</Button>
}
/>
}
>
<PopoverHeader className="flex-row items-start justify-between gap-2">
<div className="flex items-center gap-2.5">
<span
className="grid size-9 place-items-center rounded-lg border border-border bg-muted/60 text-sm font-semibold shadow-xs"
style={{ color: state.textColor ?? "currentColor" }}
>
<Trans comment="Clear the text color">Clear</Trans>
</Button>
</PopoverHeader>
<Separator />
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-muted-foreground">
<Trans>Presets</Trans>
A
</span>
<div className="grid grid-cols-8 gap-2 rounded-lg bg-muted/40 p-2">
{richTextColorOptions.map((color) => (
<button
key={color}
type="button"
className={cn(
"size-7 rounded-full border border-border shadow-xs ring-offset-background transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-hidden",
state.textColor === color && "ring-2 ring-ring ring-offset-2",
)}
style={{ backgroundColor: color }}
title={color}
aria-label={t`Use color ${color}`}
aria-pressed={state.textColor === color}
onClick={() => state.setTextColor(color)}
/>
))}
<div className="flex flex-col gap-0.5">
<PopoverTitle>
<Trans>Text Color</Trans>
</PopoverTitle>
<span className="text-xs text-muted-foreground">
<Trans comment="Preset or custom shade refer to the color picker">
Choose a preset or custom shade.
</Trans>
</span>
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-muted-foreground">
<Trans>Custom</Trans>
</span>
<div className={styles.color_picker}>
<ReactColorColorful
color={rgbaStringToHsva(state.textColor ?? defaultTextColor)}
onChange={(color: ColorResult) => {
state.setTextColor(hsvaToRgbaString(color.hsva));
}}
/>
</div>
</div>
</PopoverContent>
</Popover>
<Button
size="xs"
variant="ghost"
className="shrink-0"
onClick={state.unsetTextColor}
disabled={!state.textColor}
>
<Trans comment="Clear the text color">Clear</Trans>
</Button>
</PopoverHeader>
</ColorPicker>
<div className="mx-1 h-5 w-px bg-border" />
@@ -13,7 +13,7 @@ export function InterestsItem({ className, ...item }: InterestsItemProps) {
<div className={cn("interests-item", className)}>
{/* Header */}
<div className="section-item-header interests-item-header flex items-center gap-x-1.5">
<PageIcon icon={item.icon} className="section-item-icon interests-item-icon" />
<PageIcon icon={item.icon} color={item.iconColor} className="section-item-icon interests-item-icon" />
<strong className="section-item-title interests-item-name">{item.name}</strong>
</div>
@@ -15,7 +15,7 @@ export function ProfilesItem({ className, ...item }: ProfilesItemProps) {
<div className={cn("profiles-item", className)}>
{/* Header */}
<div className="section-item-header profiles-item-header flex items-center gap-x-1.5">
<PageIcon icon={item.icon} className="section-item-icon profiles-item-icon" />
<PageIcon icon={item.icon} color={item.iconColor} className="section-item-icon profiles-item-icon" />
<LinkedTitle
title={item.network}
website={item.website}
@@ -14,7 +14,7 @@ export function SkillsItem({ className, ...item }: SkillsItemProps) {
<div className={cn("skills-item", className)}>
{/* Header */}
<div className="section-item-header flex items-center gap-x-1.5">
<PageIcon icon={item.icon} className="section-item-icon skills-item-icon" />
<PageIcon icon={item.icon} color={item.iconColor} className="section-item-icon skills-item-icon" />
<strong className="section-item-title skills-item-name">{item.name}</strong>
</div>
+2 -2
View File
@@ -5,7 +5,7 @@ import { cn } from "@/utils/style";
import type { ExtendedIconProps } from "../preview";
export function PageIcon({ icon, className }: { icon: string; className?: string }) {
export function PageIcon({ icon, color, className }: { icon: string; color?: string; className?: string }) {
const iconContext = use<ExtendedIconProps>(IconContext);
if (!icon || iconContext.hidden) return null;
@@ -13,7 +13,7 @@ export function PageIcon({ icon, className }: { icon: string; className?: string
return (
<i
className={cn("ph shrink-0", `ph-${icon}`, className)}
style={{ fontSize: `${iconContext.size}px`, color: iconContext.color }}
style={{ fontSize: `${iconContext.size}px`, color: color || iconContext.color }}
/>
);
}
+11 -1
View File
@@ -115,6 +115,7 @@ describe("useResumeStore — updateResumeData", () => {
id: "s1",
hidden: false,
icon: "",
iconColor: "",
name: "TypeScript",
proficiency: "Advanced",
level: 4,
@@ -246,7 +247,16 @@ describe("useResumeStore — edge cases", () => {
it("can remove all items from a section", () => {
const resume = makeResume();
resume.data.sections.skills.items = [
{ id: "s1", hidden: false, icon: "", name: "JS", proficiency: "", level: 0, keywords: [] },
{
id: "s1",
hidden: false,
icon: "",
iconColor: "",
name: "JS",
proficiency: "",
level: 0,
keywords: [],
},
];
useResumeStore.getState().initialize(resume);
+1 -1
View File
@@ -99,7 +99,7 @@ function ComboboxContent({
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-60 origin-(--transform-origin) overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"group/combobox-content relative max-h-(--available-height) max-w-fit min-w-(--anchor-width) origin-(--transform-origin) overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
+1 -1
View File
@@ -44,7 +44,7 @@ function ContextMenuContent({
<ContextMenuPrimitive.Popup
data-slot="context-menu-content"
className={cn(
"z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"z-50 max-h-(--available-height) max-w-fit min-w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
+2 -1
View File
@@ -37,7 +37,8 @@ function DropdownMenuContent({
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 motion-reduce:duration-0 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 motion-reduce:data-open:animate-none data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95 motion-reduce:data-closed:animate-none",
"z-50 max-h-(--available-height) max-w-fit min-w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 motion-reduce:duration-0 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 motion-reduce:data-open:animate-none data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95 motion-reduce:data-closed:animate-none",
className,
)}
{...props}
+36 -2
View File
@@ -9,12 +9,14 @@ import { useForm, useFormContext, useFormState } from "react-hook-form";
import type { DialogProps } from "@/dialogs/store";
import { ChipInput } from "@/components/input/chip-input";
import { ColorPicker } from "@/components/input/color-picker";
import { IconPicker } from "@/components/input/icon-picker";
import { useResumeStore } from "@/components/resume/store/resume";
import { Button } from "@/components/ui/button";
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PopoverTrigger } from "@/components/ui/popover";
import { useDialogStore } from "@/dialogs/store";
import { useFormBlocker } from "@/hooks/use-form-blocker";
import { interestItemSchema } from "@/schema/resume/data";
@@ -36,6 +38,7 @@ export function CreateInterestDialog({ data }: DialogProps<"resume.sections.inte
id: generateId(),
hidden: data?.item?.hidden ?? false,
icon: data?.item?.icon ?? "acorn",
iconColor: data?.item?.iconColor ?? "",
name: data?.item?.name ?? "",
keywords: data?.item?.keywords ?? [],
},
@@ -89,6 +92,7 @@ export function UpdateInterestDialog({ data }: DialogProps<"resume.sections.inte
id: data.item.id,
hidden: data.item.hidden,
icon: data.item.icon,
iconColor: data.item.iconColor,
name: data.item.name,
keywords: data.item.keywords,
},
@@ -150,7 +154,11 @@ function InterestForm() {
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
<IconPicker
{...field}
popoverProps={{ modal: true }}
className="rounded-r-none border-e-0 border-input"
/>
}
/>
</FormItem>
@@ -165,11 +173,37 @@ function InterestForm() {
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormControl render={<Input className="rounded-s-none rounded-e-none" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="iconColor"
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<ColorPicker
{...field}
value={field.value}
onChange={field.onChange}
trigger={
<PopoverTrigger className="h-9 rounded-e border-y border-e border-input px-2">
<div
className="size-4 shrink-0 cursor-pointer rounded-full border border-foreground/60 transition-all hover:scale-105 focus-visible:outline-hidden"
style={{ backgroundColor: field.value ?? "currentColor" }}
/>
</PopoverTrigger>
}
/>
}
/>
</FormItem>
)}
/>
</div>
<FormField
+36 -2
View File
@@ -6,6 +6,7 @@ import { AtIcon, PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react";
import { useMemo } from "react";
import { useForm, useFormContext, useFormState } from "react-hook-form";
import { ColorPicker } from "@/components/input/color-picker";
import { IconPicker } from "@/components/input/icon-picker";
import { URLInput } from "@/components/input/url-input";
import { useResumeStore } from "@/components/resume/store/resume";
@@ -14,6 +15,7 @@ import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTit
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group";
import { PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { type DialogProps, useDialogStore } from "@/dialogs/store";
import { useFormBlocker } from "@/hooks/use-form-blocker";
@@ -37,6 +39,7 @@ export function CreateProfileDialog({ data }: DialogProps<"resume.sections.profi
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
icon: data?.item?.icon ?? "acorn",
iconColor: data?.item?.iconColor ?? "",
network: data?.item?.network ?? "",
username: data?.item?.username ?? "",
website: data?.item?.website ?? { url: "", label: "" },
@@ -92,6 +95,7 @@ export function UpdateProfileDialog({ data }: DialogProps<"resume.sections.profi
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
icon: data.item.icon,
iconColor: data.item.iconColor,
network: data.item.network,
username: data.item.username,
website: data.item.website,
@@ -154,7 +158,11 @@ function ProfileForm() {
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
<IconPicker
{...field}
popoverProps={{ modal: true }}
className="rounded-r-none border-e-0 border-input"
/>
}
/>
</FormItem>
@@ -169,11 +177,37 @@ function ProfileForm() {
<FormLabel>
<Trans>Network</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormControl render={<Input className="rounded-s-none rounded-e-none" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="iconColor"
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<ColorPicker
{...field}
value={field.value}
onChange={field.onChange}
trigger={
<PopoverTrigger className="h-9 rounded-e border-y border-e border-input px-2">
<div
className="size-4 shrink-0 cursor-pointer rounded-full border border-foreground/60 transition-all hover:scale-105 focus-visible:outline-hidden"
style={{ backgroundColor: field.value ?? "currentColor" }}
/>
</PopoverTrigger>
}
/>
}
/>
</FormItem>
)}
/>
</div>
<FormField
+36 -2
View File
@@ -10,12 +10,14 @@ import { useForm, useFormContext, useFormState } from "react-hook-form";
import type { DialogProps } from "@/dialogs/store";
import { ChipInput } from "@/components/input/chip-input";
import { ColorPicker } from "@/components/input/color-picker";
import { IconPicker } from "@/components/input/icon-picker";
import { useResumeStore } from "@/components/resume/store/resume";
import { Button } from "@/components/ui/button";
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PopoverTrigger } from "@/components/ui/popover";
import { Slider } from "@/components/ui/slider";
import { useDialogStore } from "@/dialogs/store";
import { useFormBlocker } from "@/hooks/use-form-blocker";
@@ -38,6 +40,7 @@ export function CreateSkillDialog({ data }: DialogProps<"resume.sections.skills.
id: generateId(),
hidden: data?.item?.hidden ?? false,
icon: data?.item?.icon ?? "acorn",
iconColor: data?.item?.iconColor ?? "",
name: data?.item?.name ?? "",
proficiency: data?.item?.proficiency ?? "",
level: data?.item?.level ?? 0,
@@ -93,6 +96,7 @@ export function UpdateSkillDialog({ data }: DialogProps<"resume.sections.skills.
id: data.item.id,
hidden: data.item.hidden,
icon: data.item.icon,
iconColor: data.item.iconColor,
name: data.item.name,
proficiency: data.item.proficiency,
level: data.item.level,
@@ -156,7 +160,11 @@ function SkillForm() {
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
<IconPicker
{...field}
popoverProps={{ modal: true }}
className="rounded-r-none border-e-0 border-input"
/>
}
/>
</FormItem>
@@ -171,11 +179,37 @@ function SkillForm() {
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormControl render={<Input className="rounded-s-none rounded-e-none" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="iconColor"
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<ColorPicker
{...field}
value={field.value}
onChange={field.onChange}
trigger={
<PopoverTrigger className="h-9 rounded-e border-y border-e border-input px-2">
<div
className="size-4 shrink-0 cursor-pointer rounded-full border border-foreground/60 transition-all hover:scale-105 focus-visible:outline-hidden"
style={{ backgroundColor: field.value ?? "currentColor" }}
/>
</PopoverTrigger>
}
/>
}
/>
</FormItem>
)}
/>
</div>
<FormField
+1
View File
@@ -68,6 +68,7 @@ describe("DialogStore — openDialog", () => {
id: "s1",
hidden: false,
icon: "star",
iconColor: "",
name: "TypeScript",
proficiency: "Advanced",
level: 4,
+3
View File
@@ -280,6 +280,7 @@ export class JSONResumeImporter {
id: generateId(),
hidden: false,
icon: "star",
iconColor: "",
name: skill.name || "",
proficiency: skill.level || "",
level: parseLevel(skill.level),
@@ -314,6 +315,7 @@ export class JSONResumeImporter {
id: generateId(),
hidden: false,
icon: "star",
iconColor: "",
name: interest.name || "",
keywords: interest.keywords || [],
})),
@@ -420,6 +422,7 @@ export class JSONResumeImporter {
id: generateId(),
hidden: false,
icon: getNetworkIcon(profile.network),
iconColor: "",
network: profile.network || "",
username: profile.username || "",
website: createUrl(profile.url, profile.username || profile.network),
@@ -456,6 +456,7 @@ export class ReactiveResumeV4JSONImporter {
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: item.icon ?? "",
iconColor: "",
network: item.network!,
username: item.username ?? "",
website: {
@@ -535,6 +536,7 @@ export class ReactiveResumeV4JSONImporter {
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: "",
iconColor: "",
name: item.name!,
proficiency: item.description ?? "",
level: clampLevel(item.level ?? 0),
@@ -565,6 +567,7 @@ export class ReactiveResumeV4JSONImporter {
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: "",
iconColor: "",
name: item.name!,
keywords: item.keywords ?? [],
})),
@@ -121,7 +121,7 @@ export function SectionDropdownMenu({ type }: Props) {
}
/>
<DropdownMenuContent>
<DropdownMenuContent align="end">
{type !== "summary" && (
<>
<DropdownMenuGroup>
+1 -1
View File
@@ -14,7 +14,7 @@ function handler() {
"Surrogate-Control": "max-age=86400",
"X-Content-Type-Options": "nosniff",
"X-Robots-Tag": "index, follow",
ETag: `"v5.0.0"`,
ETag: __APP_VERSION__,
Vary: "Accept",
},
});
+10
View File
@@ -8,6 +8,13 @@ export const iconSchema = z
"The icon to display for the custom field. Must be a valid icon name from @phosphor-icons/web icon set, or an empty string to hide. Default to '' (empty string) when unsure which icons are available.",
);
export const iconColorSchema = z
.string()
.catch("")
.describe(
"Optional custom color for the icon, defined as rgba(r, g, b, a). Leave blank to use the template default icon color.",
);
export const itemOptionsSchema = z
.object({
showLinkInTitle: z
@@ -164,6 +171,7 @@ export const experienceItemSchema = baseItemSchema.extend({
export const interestItemSchema = baseItemSchema.extend({
icon: iconSchema,
iconColor: iconColorSchema,
name: z.string().min(1).describe("The name of the interest/hobby."),
keywords: z
.array(z.string())
@@ -190,6 +198,7 @@ export const languageItemSchema = baseItemSchema.extend({
export const profileItemSchema = baseItemSchema.extend({
icon: iconSchema,
iconColor: iconColorSchema,
network: z.string().min(1).describe("The name of the network or platform."),
username: z.string().describe("The username of the author on the network or platform."),
website: urlSchema.describe("The link to the profile of the author on the network or platform, if any."),
@@ -224,6 +233,7 @@ export const referenceItemSchema = baseItemSchema.extend({
export const skillItemSchema = baseItemSchema.extend({
icon: iconSchema,
iconColor: iconColorSchema,
name: z.string().min(1).describe("The name of the skill."),
proficiency: z
.string()
+12
View File
@@ -55,6 +55,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-3d42ddc9b4d8",
hidden: false,
icon: "github-logo",
iconColor: "",
network: "GitHub",
username: "dkowalski-dev",
website: {
@@ -66,6 +67,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-43c470b77f4a",
hidden: false,
icon: "linkedin-logo",
iconColor: "",
network: "LinkedIn",
username: "davidkowalski",
website: {
@@ -173,6 +175,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-5a52dcf50ed4",
hidden: false,
icon: "code",
iconColor: "",
name: "Unity Engine",
proficiency: "Expert",
level: 5,
@@ -182,6 +185,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-5e8bb7cacbc8",
hidden: false,
icon: "brackets-curly",
iconColor: "",
name: "Unreal Engine",
proficiency: "Advanced",
level: 4,
@@ -191,6 +195,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-622f9d41da55",
hidden: false,
icon: "cpu",
iconColor: "",
name: "Programming Languages",
proficiency: "Expert",
level: 5,
@@ -200,6 +205,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-6574ab6814bd",
hidden: false,
icon: "brain",
iconColor: "",
name: "Game AI",
proficiency: "Advanced",
level: 4,
@@ -209,6 +215,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-6a8e22bec684",
hidden: false,
icon: "shooting-star",
iconColor: "",
name: "Physics & Mathematics",
proficiency: "Advanced",
level: 4,
@@ -218,6 +225,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-6d8bf7be7514",
hidden: true,
icon: "chart-line-up",
iconColor: "",
name: "Performance Optimization",
proficiency: "Advanced",
level: 4,
@@ -255,6 +263,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-7821b4de95f7",
hidden: false,
icon: "game-controller",
iconColor: "",
name: "Game Design",
keywords: ["Mechanics", "Level Design", "Player Psychology"],
},
@@ -262,6 +271,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-7c64c1a607d3",
hidden: false,
icon: "robot",
iconColor: "",
name: "AI & Procedural Generation",
keywords: ["PCG", "Machine Learning", "Emergent Gameplay"],
},
@@ -269,6 +279,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-80bccce3c0ef",
hidden: false,
icon: "book-open",
iconColor: "",
name: "Indie Game Development",
keywords: ["Solo Dev", "Game Jams", "Community"],
},
@@ -276,6 +287,7 @@ export const sampleResumeData: ResumeData = {
id: "019bef5a-93e4-7746-ad39-84bb7e9af005",
hidden: false,
icon: "pen-nib",
iconColor: "",
name: "Technical Art",
keywords: ["Shaders", "VFX", "Optimization"],
},
+646 -165
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -137,6 +137,7 @@ describe("buildDocument", () => {
id: "skill-1",
hidden: false,
icon: "",
iconColor: "",
name: "TypeScript",
proficiency: "Advanced",
level: 4,
@@ -198,6 +198,7 @@ describe("renderBuiltInSection — skills", () => {
id: "s1",
hidden: false,
icon: "",
iconColor: "",
name: "TypeScript",
proficiency: "Advanced",
level: 4,
@@ -213,7 +214,16 @@ describe("renderBuiltInSection — skills", () => {
const sections = freshSections();
sections.skills.title = "Skills";
sections.skills.items = [
{ id: "s1", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] },
{
id: "s1",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
},
];
const result = renderBuiltInSection("skills", sections.skills, COLOR_HEX);
@@ -315,6 +325,7 @@ describe("renderBuiltInSection — other sections", () => {
id: "p1",
hidden: false,
icon: "",
iconColor: "",
network: "GitHub",
username: "janedoe",
website: { url: "https://github.com/janedoe", label: "GitHub" },
@@ -423,7 +434,16 @@ describe("renderBuiltInSection — other sections", () => {
it("renders interests with keywords", () => {
const sections = freshSections();
sections.interests.title = "Interests";
sections.interests.items = [{ id: "i1", hidden: false, icon: "", name: "Gaming", keywords: ["RPG", "Strategy"] }];
sections.interests.items = [
{
id: "i1",
hidden: false,
icon: "",
iconColor: "",
name: "Gaming",
keywords: ["RPG", "Strategy"],
},
];
const result = renderBuiltInSection("interests", sections.interests, COLOR_HEX);
expect(result.length).toBe(2);
@@ -486,7 +506,18 @@ describe("renderCustomSection", () => {
title: "Hidden",
columns: 1,
hidden: true,
items: [{ id: "ci1", hidden: false, icon: "", name: "Skill", proficiency: "", level: 0, keywords: [] }],
items: [
{
id: "ci1",
hidden: false,
icon: "",
iconColor: "",
name: "Skill",
proficiency: "",
level: 0,
keywords: [],
},
],
};
expect(renderCustomSection(custom, COLOR_HEX)).toHaveLength(0);
@@ -499,7 +530,18 @@ describe("renderCustomSection", () => {
title: "Skills",
columns: 1,
hidden: false,
items: [{ id: "ci1", hidden: true, icon: "", name: "Skill", proficiency: "", level: 0, keywords: [] }],
items: [
{
id: "ci1",
hidden: true,
icon: "",
iconColor: "",
name: "Skill",
proficiency: "",
level: 0,
keywords: [],
},
],
};
expect(renderCustomSection(custom, COLOR_HEX)).toHaveLength(0);
@@ -548,7 +590,16 @@ describe("renderCustomSection", () => {
columns: 1,
hidden: false,
items: [
{ id: "ci1", hidden: false, icon: "", name: "Leadership", proficiency: "Expert", level: 5, keywords: [] },
{
id: "ci1",
hidden: false,
icon: "",
iconColor: "",
name: "Leadership",
proficiency: "Expert",
level: 5,
keywords: [],
},
],
};
@@ -575,7 +626,16 @@ describe("setRenderConfig", () => {
const sections = freshSections();
sections.skills.title = "Skills";
sections.skills.items = [
{ id: "s1", hidden: false, icon: "", name: "Test", proficiency: "", level: 0, keywords: [] },
{
id: "s1",
hidden: false,
icon: "",
iconColor: "",
name: "Test",
proficiency: "",
level: 0,
keywords: [],
},
];
const result = renderBuiltInSection("skills", sections.skills, "#0000FF");
+162 -16
View File
@@ -38,9 +38,36 @@ function freshResume(): ResumeData {
function resumeWithSkills(): ResumeData {
const data = freshResume();
data.sections.skills.items = [
{ id: "s1", hidden: false, icon: "", name: "JS", proficiency: "Advanced", level: 4, keywords: ["frontend"] },
{ id: "s2", hidden: false, icon: "", name: "TS", proficiency: "Intermediate", level: 3, keywords: [] },
{ id: "s3", hidden: false, icon: "", name: "Rust", proficiency: "Beginner", level: 1, keywords: [] },
{
id: "s1",
hidden: false,
icon: "",
iconColor: "",
name: "JS",
proficiency: "Advanced",
level: 4,
keywords: ["frontend"],
},
{
id: "s2",
hidden: false,
icon: "",
iconColor: "",
name: "TS",
proficiency: "Intermediate",
level: 3,
keywords: [],
},
{
id: "s3",
hidden: false,
icon: "",
iconColor: "",
name: "Rust",
proficiency: "Beginner",
level: 1,
keywords: [],
},
];
return data;
}
@@ -74,7 +101,18 @@ function resumeWithCustomSections(): ResumeData {
title: "Soft Skills",
columns: 1,
hidden: false,
items: [{ id: "cs1", hidden: false, icon: "", name: "Leadership", proficiency: "", level: 0, keywords: [] }],
items: [
{
id: "cs1",
hidden: false,
icon: "",
iconColor: "",
name: "Leadership",
proficiency: "",
level: 0,
keywords: [],
},
],
},
];
// Add custom sections to the layout
@@ -306,7 +344,16 @@ describe("removeItemFromSource", () => {
describe("addItemToSection", () => {
it("adds an item to a standard section", () => {
const data = freshResume();
const newItem = { id: "new-1", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] };
const newItem = {
id: "new-1",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
};
const result = produce(data, (draft) => {
addItemToSection(draft, newItem, "skills", "skills");
@@ -318,7 +365,16 @@ describe("addItemToSection", () => {
it("adds an item to a custom section", () => {
const data = resumeWithCustomSections();
const newItem = { id: "new-2", hidden: false, icon: "", name: "Teamwork", proficiency: "", level: 0, keywords: [] };
const newItem = {
id: "new-2",
hidden: false,
icon: "",
iconColor: "",
name: "Teamwork",
proficiency: "",
level: 0,
keywords: [],
};
const result = produce(data, (draft) => {
addItemToSection(draft, newItem, "custom-skills-1", "skills");
@@ -330,7 +386,16 @@ describe("addItemToSection", () => {
it("does nothing when target custom section doesn't exist", () => {
const data = freshResume();
const newItem = { id: "new-3", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] };
const newItem = {
id: "new-3",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
};
const result = produce(data, (draft) => {
addItemToSection(draft, newItem, "nonexistent-section", "skills");
@@ -342,7 +407,16 @@ describe("addItemToSection", () => {
it("appends to the end of an existing items array", () => {
const data = resumeWithSkills();
const newItem = { id: "s4", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] };
const newItem = {
id: "s4",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
};
const result = produce(data, (draft) => {
addItemToSection(draft, newItem, "skills", "skills");
@@ -360,7 +434,16 @@ describe("addItemToSection", () => {
describe("createCustomSectionWithItem", () => {
it("creates a new custom section with the given item", () => {
const data = freshResume();
const item = { id: "item-1", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] };
const item = {
id: "item-1",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
};
const result = produce(data, (draft) => {
const sectionId = createCustomSectionWithItem(draft, item, "skills", "Tech Skills", 0);
@@ -377,7 +460,16 @@ describe("createCustomSectionWithItem", () => {
it("adds the new section to the target page's main column", () => {
const data = freshResume();
const item = { id: "item-1", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] };
const item = {
id: "item-1",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
};
const mainLengthBefore = data.metadata.layout.pages[0].main.length;
const result = produce(data, (draft) => {
@@ -390,7 +482,16 @@ describe("createCustomSectionWithItem", () => {
it("handles targeting a specific page index", () => {
const data = resumeWithMultiplePages();
const item = { id: "item-2", hidden: false, icon: "", name: "Python", proficiency: "", level: 0, keywords: [] };
const item = {
id: "item-2",
hidden: false,
icon: "",
iconColor: "",
name: "Python",
proficiency: "",
level: 0,
keywords: [],
};
const result = produce(data, (draft) => {
createCustomSectionWithItem(draft, item, "skills", "More Skills", 1);
@@ -403,7 +504,16 @@ describe("createCustomSectionWithItem", () => {
it("handles out-of-bounds page index gracefully (no crash)", () => {
const data = freshResume();
const item = { id: "item-3", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] };
const item = {
id: "item-3",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
};
// Should not throw — the page just doesn't exist so nothing is pushed
const result = produce(data, (draft) => {
@@ -424,7 +534,16 @@ describe("createCustomSectionWithItem", () => {
describe("createPageWithSection", () => {
it("creates a new page with a custom section", () => {
const data = freshResume();
const item = { id: "item-1", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] };
const item = {
id: "item-1",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
};
const pagesBefore = data.metadata.layout.pages.length;
const result = produce(data, (draft) => {
@@ -440,7 +559,16 @@ describe("createPageWithSection", () => {
it("creates the custom section with correct properties", () => {
const data = freshResume();
const item = { id: "item-1", hidden: false, icon: "", name: "Go", proficiency: "", level: 0, keywords: [] };
const item = {
id: "item-1",
hidden: false,
icon: "",
iconColor: "",
name: "Go",
proficiency: "",
level: 0,
keywords: [],
};
const result = produce(data, (draft) => {
createPageWithSection(draft, item, "skills", "Page 2 Skills");
@@ -454,8 +582,26 @@ describe("createPageWithSection", () => {
it("can create multiple pages in sequence", () => {
const data = freshResume();
const item1 = { id: "i1", hidden: false, icon: "", name: "A", proficiency: "", level: 0, keywords: [] };
const item2 = { id: "i2", hidden: false, icon: "", name: "B", proficiency: "", level: 0, keywords: [] };
const item1 = {
id: "i1",
hidden: false,
icon: "",
iconColor: "",
name: "A",
proficiency: "",
level: 0,
keywords: [],
};
const item2 = {
id: "i2",
hidden: false,
icon: "",
iconColor: "",
name: "B",
proficiency: "",
level: 0,
keywords: [],
};
const result = produce(data, (draft) => {
createPageWithSection(draft, item1, "skills", "Page 2");
+4 -4
View File
@@ -147,8 +147,8 @@ describe("applyResumePatches", () => {
it("removes an item from a section's items array", () => {
const data = freshResume();
data.sections.skills.items = [
{ id: "s1", hidden: false, icon: "", name: "JS", proficiency: "", level: 0, keywords: [] },
{ id: "s2", hidden: false, icon: "", name: "TS", proficiency: "", level: 0, keywords: [] },
{ id: "s1", hidden: false, icon: "", iconColor: "", name: "JS", proficiency: "", level: 0, keywords: [] },
{ id: "s2", hidden: false, icon: "", iconColor: "", name: "TS", proficiency: "", level: 0, keywords: [] },
];
const result = applyResumePatches(data, [{ op: "remove", path: "/sections/skills/items/0" }]);
@@ -455,8 +455,8 @@ describe("applyResumePatches", () => {
it("replaces a specific item in an array", () => {
const data = freshResume();
data.sections.skills.items = [
{ id: "s1", hidden: false, icon: "", name: "JS", proficiency: "", level: 0, keywords: [] },
{ id: "s2", hidden: false, icon: "", name: "TS", proficiency: "", level: 0, keywords: [] },
{ id: "s1", hidden: false, icon: "", iconColor: "", name: "JS", proficiency: "", level: 0, keywords: [] },
{ id: "s2", hidden: false, icon: "", iconColor: "", name: "TS", proficiency: "", level: 0, keywords: [] },
];
const result = applyResumePatches(data, [
+12 -1
View File
@@ -53,7 +53,18 @@ function resumeWithCustomSection(): ResumeData {
title: "Soft Skills",
columns: 1,
hidden: false,
items: [{ id: "cs-1", hidden: false, icon: "", name: "Leadership", proficiency: "", level: 0, keywords: [] }],
items: [
{
id: "cs-1",
hidden: false,
icon: "",
iconColor: "",
name: "Leadership",
proficiency: "",
level: 0,
keywords: [],
},
],
},
];
return data;
+3
View File
@@ -67,6 +67,7 @@ function makeResumeData(overrides?: {
id: "skill-0",
hidden: false,
icon: "",
iconColor: "",
name: "JavaScript",
proficiency: "Advanced",
level: 4,
@@ -76,6 +77,7 @@ function makeResumeData(overrides?: {
id: "skill-1",
hidden: false,
icon: "",
iconColor: "",
name: "Python",
proficiency: "Intermediate",
level: 3,
@@ -85,6 +87,7 @@ function makeResumeData(overrides?: {
id: "skill-2",
hidden: true,
icon: "",
iconColor: "",
name: "Photography",
proficiency: "",
level: 0,