mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
feat(pdf): roll out shared RTL layout to all templates
Introduce createRtlStyleHelpers and a single rtl flag on RenderProvider, migrate every template page to mirrored layout styles, and rename alignRight to alignEnd. Fix plain rich text rendering via PdfText paragraph renderers and map legacy Times New Roman to Times-Roman. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -261,7 +261,7 @@ describe("buildResumeFontFamily", () => {
|
||||
|
||||
describe("legacy font compatibility (#2989)", () => {
|
||||
it.each([
|
||||
["Times New Roman", "Tinos"],
|
||||
["Times New Roman", "Times-Roman"],
|
||||
["Arial", "Arimo"],
|
||||
["Garamond", "EB Garamond"],
|
||||
["Calibri", "Carlito"],
|
||||
@@ -277,7 +277,7 @@ describe("legacy font compatibility (#2989)", () => {
|
||||
});
|
||||
|
||||
it("every alias target is actually registered as a known font", () => {
|
||||
const aliasTargets = ["Tinos", "Arimo", "EB Garamond", "Carlito"];
|
||||
const aliasTargets = ["Times-Roman", "Tinos", "Arimo", "EB Garamond", "Carlito"];
|
||||
for (const target of aliasTargets) {
|
||||
expect(getFont(target), `alias target ${target} must be a known font`).toBeDefined();
|
||||
}
|
||||
@@ -286,7 +286,7 @@ describe("legacy font compatibility (#2989)", () => {
|
||||
it("getFont resolves a legacy family to its alias target", () => {
|
||||
const tnr = getFont("Times New Roman");
|
||||
expect(tnr).toBeDefined();
|
||||
expect(tnr?.family).toBe("Tinos");
|
||||
expect(tnr?.family).toBe("Times-Roman");
|
||||
});
|
||||
|
||||
it("getFont still returns the direct font when both legacy and direct lookup would succeed", () => {
|
||||
|
||||
@@ -132,7 +132,7 @@ const legacyFontAliases: Record<string, string> = {
|
||||
Cambria: "Tinos",
|
||||
Calibri: "Carlito",
|
||||
Garamond: "EB Garamond",
|
||||
"Times New Roman": "Tinos",
|
||||
"Times New Roman": "Times-Roman",
|
||||
};
|
||||
|
||||
export function resolveLegacyFontAlias(family: string): string | null {
|
||||
|
||||
@@ -2,9 +2,11 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { ReactNode } from "react";
|
||||
import type { SectionTitleResolver } from "./section-title";
|
||||
import { createContext, use } from "react";
|
||||
import { isRTL } from "@reactive-resume/utils/locale";
|
||||
|
||||
type RenderContextValue = ResumeData & {
|
||||
resolveSectionTitle?: SectionTitleResolver | undefined;
|
||||
rtl: boolean;
|
||||
};
|
||||
|
||||
const RenderContext = createContext<RenderContextValue | null>(null);
|
||||
@@ -16,7 +18,9 @@ export type RenderProviderProps = {
|
||||
};
|
||||
|
||||
export const RenderProvider = ({ data, resolveSectionTitle, children }: RenderProviderProps) => {
|
||||
return <RenderContext.Provider value={{ ...data, resolveSectionTitle }}>{children}</RenderContext.Provider>;
|
||||
const rtl = isRTL(data.metadata.page.locale);
|
||||
|
||||
return <RenderContext.Provider value={{ ...data, resolveSectionTitle, rtl }}>{children}</RenderContext.Provider>;
|
||||
};
|
||||
|
||||
export const useRender = (): RenderContextValue => {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { templateSchema } from "@reactive-resume/schema/templates";
|
||||
|
||||
const templatePages = templateSchema.options.map(
|
||||
(template) =>
|
||||
[
|
||||
template,
|
||||
fileURLToPath(new URL(`./templates/${template}/${capitalize(template)}Page.tsx`, import.meta.url)),
|
||||
] as const,
|
||||
);
|
||||
|
||||
function capitalize(template: string): string {
|
||||
return template.charAt(0).toUpperCase() + template.slice(1);
|
||||
}
|
||||
|
||||
describe("RTL PDF fixture", () => {
|
||||
it.each(templatePages)("%s wires shared RTL helpers and alignEnd slot", (_template, pagePath) => {
|
||||
const source = readFileSync(pagePath, "utf8");
|
||||
|
||||
expect(source).toContain("createRtlStyleHelpers");
|
||||
expect(source).toContain("alignEnd");
|
||||
expect(source).not.toContain("alignRight");
|
||||
expect(source).not.toContain('from "@reactive-resume/utils/locale"');
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
|
||||
@@ -131,9 +132,10 @@ const Header = ({ styles }: { styles: AzurillStyles }) => {
|
||||
};
|
||||
|
||||
const useAzurillTemplate = (): AzurillTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -146,6 +148,7 @@ const useAzurillTemplate = (): AzurillTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -160,6 +163,7 @@ const useAzurillTemplate = (): AzurillTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -168,13 +172,14 @@ const useAzurillTemplate = (): AzurillTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
inline: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
@@ -198,32 +203,29 @@ const useAzurillTemplate = (): AzurillTemplate => {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
richListItemMarker: {
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: "right",
|
||||
...bodyText,
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: r.listMarkerTextAlign,
|
||||
},
|
||||
richListItemContent: {
|
||||
flex: 1,
|
||||
...bodyText,
|
||||
},
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: {
|
||||
textAlign: "right",
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
flexShrink: 1,
|
||||
alignEnd: {
|
||||
...r.alignEnd,
|
||||
},
|
||||
sectionHeading: {
|
||||
color: primary,
|
||||
},
|
||||
contentRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
},
|
||||
sidebarColumn: {},
|
||||
mainColumn: {
|
||||
@@ -260,13 +262,13 @@ const useAzurillTemplate = (): AzurillTemplate => {
|
||||
},
|
||||
headerContactRow: {
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(0.5),
|
||||
},
|
||||
headerContactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
@@ -355,5 +357,5 @@ const useAzurillTemplate = (): AzurillTemplate => {
|
||||
}),
|
||||
} satisfies AzurillStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -128,9 +129,10 @@ const Header = ({ styles }: { styles: BronzorStyles }) => {
|
||||
};
|
||||
|
||||
const useBronzorTemplate = (): BronzorTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -143,6 +145,7 @@ const useBronzorTemplate = (): BronzorTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -156,6 +159,7 @@ const useBronzorTemplate = (): BronzorTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -164,13 +168,14 @@ const useBronzorTemplate = (): BronzorTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights[0] ?? "500",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
inline: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
@@ -194,29 +199,26 @@ const useBronzorTemplate = (): BronzorTemplate => {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
richListItemMarker: {
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: "right",
|
||||
...bodyText,
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: r.listMarkerTextAlign,
|
||||
},
|
||||
richListItemContent: {
|
||||
flex: 1,
|
||||
...bodyText,
|
||||
},
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: {
|
||||
textAlign: "right",
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
flexShrink: 1,
|
||||
alignEnd: {
|
||||
...r.alignEnd,
|
||||
},
|
||||
section: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
columnGap: metrics.columnGap,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: primary,
|
||||
@@ -227,6 +229,7 @@ const useBronzorTemplate = (): BronzorTemplate => {
|
||||
flexShrink: 0,
|
||||
fontSize: metadata.typography.heading.fontSize * 0.75,
|
||||
color: primary,
|
||||
textAlign: r.sectionHeadingTextAlign,
|
||||
},
|
||||
sectionItems: {
|
||||
flex: 1,
|
||||
@@ -265,13 +268,13 @@ const useBronzorTemplate = (): BronzorTemplate => {
|
||||
},
|
||||
headerContactRow: {
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(5 / 6),
|
||||
},
|
||||
headerContactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(0.25),
|
||||
},
|
||||
@@ -283,5 +286,5 @@ const useBronzorTemplate = (): BronzorTemplate => {
|
||||
});
|
||||
|
||||
return { colors, styles: baseStyles satisfies BronzorStyles };
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
|
||||
@@ -139,9 +140,10 @@ const Header = ({ styles }: { styles: ChikoritaStyles }) => {
|
||||
};
|
||||
|
||||
const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -160,16 +162,18 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
page: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
color: foreground,
|
||||
backgroundColor: background,
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -178,13 +182,14 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
inline: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(0.25),
|
||||
},
|
||||
@@ -208,26 +213,23 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
richListItemMarker: {
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: "right",
|
||||
...bodyText,
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: r.listMarkerTextAlign,
|
||||
},
|
||||
richListItemContent: {
|
||||
flex: 1,
|
||||
...bodyText,
|
||||
},
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: {
|
||||
textAlign: "right",
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
flexShrink: 1,
|
||||
alignEnd: {
|
||||
...r.alignEnd,
|
||||
},
|
||||
section: {
|
||||
flexDirection: "column",
|
||||
@@ -256,7 +258,7 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
backgroundColor: primary,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "flex-start",
|
||||
columnGap: metrics.gapX(0.5),
|
||||
},
|
||||
@@ -277,8 +279,7 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
rowGap: metrics.gapY(0.5),
|
||||
},
|
||||
headerIdentity: {
|
||||
textAlign: "left",
|
||||
alignItems: "flex-start",
|
||||
...r.headerIdentity,
|
||||
rowGap: metrics.gapY(0.35),
|
||||
},
|
||||
headerName: {
|
||||
@@ -289,13 +290,13 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
},
|
||||
headerContactRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
rowGap: metrics.gapY(0.125),
|
||||
},
|
||||
headerContactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
@@ -330,8 +331,8 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
? { flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-start" }
|
||||
: {}),
|
||||
}),
|
||||
alignRight: (context) => ({
|
||||
...baseStyles.alignRight,
|
||||
alignEnd: (context) => ({
|
||||
...baseStyles.alignEnd,
|
||||
...(context.placement === "sidebar" ? { textAlign: "left" } : {}),
|
||||
}),
|
||||
sectionHeading: (context) => ({
|
||||
@@ -348,5 +349,5 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
|
||||
}),
|
||||
} satisfies ChikoritaStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
|
||||
@@ -166,9 +167,10 @@ const getPrimaryTint = (primaryColor: string, opacity: number): string => {
|
||||
};
|
||||
|
||||
const useDitgarTemplate = (): DitgarTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -188,16 +190,18 @@ const useDitgarTemplate = (): DitgarTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
page: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
color: foreground,
|
||||
backgroundColor: background,
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -206,24 +210,25 @@ const useDitgarTemplate = (): DitgarTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
|
||||
richParagraph: { margin: 0, ...bodyText },
|
||||
richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" },
|
||||
richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText },
|
||||
richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign },
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
section: { flexDirection: "column", rowGap: metrics.gapY(0.25) },
|
||||
sectionHeading: {
|
||||
fontSize: metadata.typography.heading.fontSize * 0.9,
|
||||
@@ -277,8 +282,7 @@ const useDitgarTemplate = (): DitgarTemplate => {
|
||||
},
|
||||
headerTitle: {},
|
||||
headerIdentity: {
|
||||
textAlign: "left",
|
||||
alignItems: "flex-start",
|
||||
...r.headerIdentity,
|
||||
rowGap: metrics.gapY(0.35),
|
||||
},
|
||||
headerName: {
|
||||
@@ -289,7 +293,7 @@ const useDitgarTemplate = (): DitgarTemplate => {
|
||||
headerText: { color: background },
|
||||
contactList: { rowGap: metrics.gapY(0.125) },
|
||||
contactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
@@ -314,8 +318,8 @@ const useDitgarTemplate = (): DitgarTemplate => {
|
||||
richParagraph: (context) => ({ ...baseStyles.richParagraph, color: foregroundFor(context) }),
|
||||
richListItemMarker: (context) => ({ ...baseStyles.richListItemMarker, color: foregroundFor(context) }),
|
||||
richListItemContent: (context) => ({ ...baseStyles.richListItemContent, color: foregroundFor(context) }),
|
||||
alignRight: (context) => ({
|
||||
...baseStyles.alignRight,
|
||||
alignEnd: (context) => ({
|
||||
...baseStyles.alignEnd,
|
||||
...(context.placement === "sidebar" ? { textAlign: "left" } : {}),
|
||||
}),
|
||||
sectionHeading: (context) => ({
|
||||
@@ -343,5 +347,5 @@ const useDitgarTemplate = (): DitgarTemplate => {
|
||||
}),
|
||||
} satisfies DitgarStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -140,9 +141,10 @@ const Header = ({ styles }: { styles: DittoStyles }) => {
|
||||
};
|
||||
|
||||
const useDittoTemplate = (): DittoTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -156,6 +158,7 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -166,6 +169,7 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -174,13 +178,14 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
inline: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
@@ -204,26 +209,23 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
richListItemMarker: {
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: "right",
|
||||
...bodyText,
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: r.listMarkerTextAlign,
|
||||
},
|
||||
richListItemContent: {
|
||||
flex: 1,
|
||||
...bodyText,
|
||||
},
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: {
|
||||
textAlign: "right",
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
flexShrink: 1,
|
||||
alignEnd: {
|
||||
...r.alignEnd,
|
||||
},
|
||||
section: {
|
||||
flexDirection: "column",
|
||||
@@ -250,7 +252,7 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
headerBand: {
|
||||
backgroundColor: primary,
|
||||
color: background,
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
...(hasPicture ? { minHeight: picture.size * 0.6 } : {}),
|
||||
},
|
||||
pictureAnchor: {
|
||||
@@ -285,8 +287,7 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
color: background,
|
||||
},
|
||||
headerIdentity: {
|
||||
textAlign: "left",
|
||||
alignItems: "flex-start",
|
||||
...r.headerIdentity,
|
||||
rowGap: metrics.gapY(0.35),
|
||||
},
|
||||
headerName: {
|
||||
@@ -298,7 +299,7 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
color: background,
|
||||
},
|
||||
contactRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
contactOffset: {
|
||||
@@ -307,7 +308,7 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
},
|
||||
contactList: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
rowGap: metrics.gapY(0.125),
|
||||
@@ -317,12 +318,12 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
contactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
contentRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
},
|
||||
sidebarColumn: {
|
||||
flexShrink: 0,
|
||||
@@ -350,8 +351,8 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
? { flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-start" }
|
||||
: {}),
|
||||
}),
|
||||
alignRight: (context) => ({
|
||||
...baseStyles.alignRight,
|
||||
alignEnd: (context) => ({
|
||||
...baseStyles.alignEnd,
|
||||
...(context.placement === "sidebar" ? { textAlign: "left" } : {}),
|
||||
}),
|
||||
icon: {
|
||||
@@ -361,5 +362,5 @@ const useDittoTemplate = (): DittoTemplate => {
|
||||
},
|
||||
} satisfies DittoStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
|
||||
@@ -163,9 +164,10 @@ const getPrimaryTint = (primaryColor: string, opacity: number): string => {
|
||||
};
|
||||
|
||||
const useGengarTemplate = (): GengarTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -185,16 +187,18 @@ const useGengarTemplate = (): GengarTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
page: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
color: foreground,
|
||||
backgroundColor: background,
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -203,13 +207,14 @@ const useGengarTemplate = (): GengarTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
inline: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
@@ -233,26 +238,23 @@ const useGengarTemplate = (): GengarTemplate => {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
richListItemMarker: {
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: "right",
|
||||
...bodyText,
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: r.listMarkerTextAlign,
|
||||
},
|
||||
richListItemContent: {
|
||||
flex: 1,
|
||||
...bodyText,
|
||||
},
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: {
|
||||
textAlign: "right",
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
flexShrink: 1,
|
||||
alignEnd: {
|
||||
...r.alignEnd,
|
||||
},
|
||||
section: {
|
||||
flexDirection: "column",
|
||||
@@ -321,8 +323,7 @@ const useGengarTemplate = (): GengarTemplate => {
|
||||
},
|
||||
headerTitle: {},
|
||||
headerIdentity: {
|
||||
textAlign: "left",
|
||||
alignItems: "flex-start",
|
||||
...r.headerIdentity,
|
||||
rowGap: metrics.gapY(0.35),
|
||||
},
|
||||
headerName: {
|
||||
@@ -337,7 +338,7 @@ const useGengarTemplate = (): GengarTemplate => {
|
||||
rowGap: metrics.gapY(0.25),
|
||||
},
|
||||
contactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
@@ -368,8 +369,8 @@ const useGengarTemplate = (): GengarTemplate => {
|
||||
? { flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-start" }
|
||||
: {}),
|
||||
}),
|
||||
alignRight: (context) => ({
|
||||
...baseStyles.alignRight,
|
||||
alignEnd: (context) => ({
|
||||
...baseStyles.alignEnd,
|
||||
...(context.placement === "sidebar" ? { textAlign: "left" } : {}),
|
||||
}),
|
||||
sectionHeading: (context) => ({
|
||||
@@ -386,5 +387,5 @@ const useGengarTemplate = (): GengarTemplate => {
|
||||
}),
|
||||
} satisfies GengarStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
|
||||
@@ -144,9 +145,10 @@ const getPrimaryTint = (primaryColor: string, opacity: number): string => {
|
||||
};
|
||||
|
||||
const useGlalieTemplate = (): GlalieTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -166,6 +168,7 @@ const useGlalieTemplate = (): GlalieTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -175,6 +178,7 @@ const useGlalieTemplate = (): GlalieTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -183,24 +187,25 @@ const useGlalieTemplate = (): GlalieTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
|
||||
richParagraph: { margin: 0, ...bodyText },
|
||||
richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" },
|
||||
richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText },
|
||||
richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign },
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
section: { flexDirection: "column", rowGap: metrics.gapY(0.25) },
|
||||
sectionHeading: { borderBottomWidth: 1, borderBottomColor: primary },
|
||||
item: { rowGap: metrics.gapY(0.125) },
|
||||
@@ -211,11 +216,11 @@ const useGlalieTemplate = (): GlalieTemplate => {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
...r.anchorToStart(0),
|
||||
width: `${metadata.layout.sidebarWidth}%`,
|
||||
backgroundColor: primaryTint,
|
||||
},
|
||||
layout: { flexDirection: "row", minHeight: "100%" },
|
||||
layout: { flexDirection: r.row, minHeight: "100%" },
|
||||
sidebarColumn: {
|
||||
zIndex: 1,
|
||||
backgroundColor: primaryTint,
|
||||
@@ -256,7 +261,7 @@ const useGlalieTemplate = (): GlalieTemplate => {
|
||||
padding: metrics.gapX(0.75),
|
||||
rowGap: metrics.gapY(0.125),
|
||||
},
|
||||
contactItem: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 6) },
|
||||
contactItem: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6) },
|
||||
});
|
||||
|
||||
const accentFor = ({ colors }: TemplateStyleContext) => colors.primary;
|
||||
@@ -284,5 +289,5 @@ const useGlalieTemplate = (): GlalieTemplate => {
|
||||
}),
|
||||
} satisfies GlalieStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -111,9 +112,10 @@ const Header = ({ styles }: { styles: KakunaStyles }) => {
|
||||
};
|
||||
|
||||
const useKakunaTemplate = (): KakunaTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -126,6 +128,7 @@ const useKakunaTemplate = (): KakunaTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -138,6 +141,7 @@ const useKakunaTemplate = (): KakunaTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -146,13 +150,14 @@ const useKakunaTemplate = (): KakunaTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
inline: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
},
|
||||
@@ -176,26 +181,23 @@ const useKakunaTemplate = (): KakunaTemplate => {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
richListItemMarker: {
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: "right",
|
||||
...bodyText,
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: r.listMarkerTextAlign,
|
||||
},
|
||||
richListItemContent: {
|
||||
flex: 1,
|
||||
...bodyText,
|
||||
},
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: {
|
||||
textAlign: "right",
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
flexShrink: 1,
|
||||
alignEnd: {
|
||||
...r.alignEnd,
|
||||
},
|
||||
section: {
|
||||
flexDirection: "column",
|
||||
@@ -261,14 +263,14 @@ const useKakunaTemplate = (): KakunaTemplate => {
|
||||
},
|
||||
contactList: {
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(0.75),
|
||||
},
|
||||
contactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
@@ -295,5 +297,5 @@ const useKakunaTemplate = (): KakunaTemplate => {
|
||||
}),
|
||||
} satisfies KakunaStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -110,9 +111,10 @@ const Header = ({ styles }: { styles: LaprasStyles }) => {
|
||||
};
|
||||
|
||||
const useLaprasTemplate = (): LaprasTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -128,6 +130,7 @@ const useLaprasTemplate = (): LaprasTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -140,6 +143,7 @@ const useLaprasTemplate = (): LaprasTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -148,24 +152,25 @@ const useLaprasTemplate = (): LaprasTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
|
||||
richParagraph: { margin: 0, ...bodyText },
|
||||
richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" },
|
||||
richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText },
|
||||
richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign },
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
section: {
|
||||
flexDirection: "column",
|
||||
rowGap: metrics.gapY(0.25),
|
||||
@@ -186,7 +191,7 @@ const useLaprasTemplate = (): LaprasTemplate => {
|
||||
levelItem: { borderColor: primary },
|
||||
levelItemActive: { backgroundColor: primary },
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1),
|
||||
borderWidth: 1,
|
||||
@@ -208,15 +213,15 @@ const useLaprasTemplate = (): LaprasTemplate => {
|
||||
transform: `rotate(${picture.rotation}deg)`,
|
||||
},
|
||||
headerTitle: { rowGap: metrics.gapY(0.5) },
|
||||
headerIdentity: { textAlign: "left", alignItems: "flex-start", rowGap: metrics.gapY(0.35) },
|
||||
headerIdentity: { ...r.headerIdentity, rowGap: metrics.gapY(0.35) },
|
||||
headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight },
|
||||
contactList: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(0.5),
|
||||
},
|
||||
contactItem: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 6) },
|
||||
contactItem: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6) },
|
||||
sectionGroup: {},
|
||||
});
|
||||
|
||||
@@ -235,5 +240,5 @@ const useLaprasTemplate = (): LaprasTemplate => {
|
||||
}),
|
||||
} satisfies LaprasStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -140,9 +141,10 @@ const getPrimaryAlpha = (primaryColor: string, opacity: number): string => {
|
||||
};
|
||||
|
||||
const useLeafishTemplate = (): LeafishTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -157,6 +159,7 @@ const useLeafishTemplate = (): LeafishTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -166,6 +169,7 @@ const useLeafishTemplate = (): LeafishTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -174,24 +178,25 @@ const useLeafishTemplate = (): LeafishTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
|
||||
richParagraph: { margin: 0, ...bodyText },
|
||||
richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" },
|
||||
richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText },
|
||||
richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign },
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
section: { flexDirection: "column", rowGap: metrics.gapY(0.25) },
|
||||
sectionHeading: { borderBottomWidth: 1, borderBottomColor: primary },
|
||||
item: { rowGap: metrics.gapY(0.125) },
|
||||
@@ -205,7 +210,7 @@ const useLeafishTemplate = (): LeafishTemplate => {
|
||||
paddingVertical: metrics.page.paddingVertical,
|
||||
},
|
||||
headerBody: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1),
|
||||
},
|
||||
@@ -213,7 +218,7 @@ const useLeafishTemplate = (): LeafishTemplate => {
|
||||
flex: 1,
|
||||
rowGap: metrics.gapY(0.5),
|
||||
},
|
||||
headerIdentity: { textAlign: "left", alignItems: "flex-start", rowGap: metrics.gapY(0.35) },
|
||||
headerIdentity: { ...r.headerIdentity, rowGap: metrics.gapY(0.35) },
|
||||
headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight },
|
||||
headerContactBand: {
|
||||
backgroundColor: primaryTintDark,
|
||||
@@ -221,13 +226,13 @@ const useLeafishTemplate = (): LeafishTemplate => {
|
||||
paddingVertical: metrics.page.paddingVertical,
|
||||
},
|
||||
contactList: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(1),
|
||||
},
|
||||
contactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
@@ -244,7 +249,7 @@ const useLeafishTemplate = (): LeafishTemplate => {
|
||||
transform: `rotate(${picture.rotation}deg)`,
|
||||
},
|
||||
body: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
columnGap: metrics.columnGap,
|
||||
paddingHorizontal: metrics.page.paddingHorizontal,
|
||||
paddingTop: metrics.page.paddingVertical,
|
||||
@@ -269,5 +274,5 @@ const useLeafishTemplate = (): LeafishTemplate => {
|
||||
}),
|
||||
} satisfies LeafishStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -115,9 +116,10 @@ const Header = ({ styles }: { styles: MeowthStyles }) => {
|
||||
};
|
||||
|
||||
const useMeowthTemplate = (): MeowthTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -129,6 +131,7 @@ const useMeowthTemplate = (): MeowthTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -141,6 +144,7 @@ const useMeowthTemplate = (): MeowthTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -149,25 +153,26 @@ const useMeowthTemplate = (): MeowthTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
|
||||
richParagraph: { margin: 0, ...bodyText },
|
||||
richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" },
|
||||
richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText },
|
||||
richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign },
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
inlineItemHeader: { flexDirection: "row", alignItems: "flex-start", columnGap: metrics.gapX(0.75) },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
inlineItemHeader: { flexDirection: r.row, alignItems: "flex-start", columnGap: metrics.gapX(0.75) },
|
||||
inlineItemHeaderLeading: { flex: 1, minWidth: 0 },
|
||||
inlineItemHeaderMiddle: { flex: 1, minWidth: 0 },
|
||||
inlineItemHeaderTrailing: { flexShrink: 0, textAlign: "right" },
|
||||
@@ -179,23 +184,24 @@ const useMeowthTemplate = (): MeowthTemplate => {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: primary,
|
||||
paddingBottom: metrics.gapY(0.125),
|
||||
textAlign: r.sectionHeadingTextAlign,
|
||||
},
|
||||
item: { rowGap: metrics.gapY(0.125) },
|
||||
levelContainer: { width: "100%" },
|
||||
levelItem: { borderColor: primary },
|
||||
levelItemActive: { backgroundColor: primary },
|
||||
header: { flexDirection: "row", alignItems: "flex-start", columnGap: metrics.gapX(1) },
|
||||
header: { flexDirection: r.row, alignItems: "flex-start", columnGap: metrics.gapX(1) },
|
||||
headerTitle: { flex: 1, rowGap: metrics.gapY(0.5) },
|
||||
headerIdentity: { textAlign: "left", alignItems: "flex-start", rowGap: metrics.gapY(0.35) },
|
||||
headerIdentity: { ...r.headerIdentity, rowGap: metrics.gapY(0.35) },
|
||||
headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight },
|
||||
headerHeadline: { opacity: 0.8 },
|
||||
contactList: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(0.75),
|
||||
},
|
||||
contactItem: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 6) },
|
||||
contactItem: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6) },
|
||||
picture: {
|
||||
width: picture.size,
|
||||
height: picture.size,
|
||||
@@ -231,5 +237,5 @@ const useMeowthTemplate = (): MeowthTemplate => {
|
||||
}),
|
||||
} satisfies MeowthStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -110,9 +111,10 @@ const Header = ({ styles }: { styles: OnyxStyles }) => {
|
||||
};
|
||||
|
||||
const useOnyxTemplate = (): OnyxTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -124,6 +126,7 @@ const useOnyxTemplate = (): OnyxTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -136,6 +139,7 @@ const useOnyxTemplate = (): OnyxTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -144,31 +148,32 @@ const useOnyxTemplate = (): OnyxTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
|
||||
richParagraph: { margin: 0, ...bodyText },
|
||||
richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" },
|
||||
richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText },
|
||||
richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign },
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
section: { flexDirection: "column", rowGap: metrics.gapY(0.25) },
|
||||
item: { rowGap: metrics.gapY(0.125) },
|
||||
levelContainer: { width: "100%" },
|
||||
levelItem: { borderColor: primary },
|
||||
levelItemActive: { backgroundColor: primary },
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1),
|
||||
borderBottomWidth: 1,
|
||||
@@ -188,15 +193,15 @@ const useOnyxTemplate = (): OnyxTemplate => {
|
||||
transform: `rotate(${picture.rotation}deg)`,
|
||||
},
|
||||
headerTitle: { rowGap: metrics.gapY(0.5) },
|
||||
headerIdentity: { textAlign: "left", alignItems: "flex-start", rowGap: metrics.gapY(0.35) },
|
||||
headerIdentity: { ...r.headerIdentity, rowGap: metrics.gapY(0.35) },
|
||||
headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight },
|
||||
contactList: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(0.75),
|
||||
},
|
||||
contactItem: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 6) },
|
||||
contactItem: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6) },
|
||||
sectionGroup: {},
|
||||
});
|
||||
|
||||
@@ -215,5 +220,5 @@ const useOnyxTemplate = (): OnyxTemplate => {
|
||||
}),
|
||||
} satisfies OnyxStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
|
||||
@@ -147,9 +148,10 @@ const Header = ({ styles, colors }: { styles: PikachuStyles; colors: TemplateCol
|
||||
};
|
||||
|
||||
const usePikachuTemplate = (): PikachuTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -162,6 +164,7 @@ const usePikachuTemplate = (): PikachuTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -173,6 +176,7 @@ const usePikachuTemplate = (): PikachuTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -181,24 +185,25 @@ const usePikachuTemplate = (): PikachuTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
|
||||
richParagraph: { margin: 0, ...bodyText },
|
||||
richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" },
|
||||
richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText },
|
||||
richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign },
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
section: { flexDirection: "column", rowGap: metrics.gapY(0.25) },
|
||||
sectionHeading: { borderBottomWidth: 1, borderBottomColor: primary },
|
||||
item: { rowGap: metrics.gapY(0.125) },
|
||||
@@ -206,7 +211,7 @@ const usePikachuTemplate = (): PikachuTemplate => {
|
||||
levelItem: { borderColor: primary },
|
||||
levelItemActive: { backgroundColor: primary },
|
||||
layout: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
columnGap: metrics.columnGap,
|
||||
},
|
||||
sidebarColumn: {
|
||||
@@ -217,7 +222,7 @@ const usePikachuTemplate = (): PikachuTemplate => {
|
||||
flex: 1,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1),
|
||||
},
|
||||
@@ -237,8 +242,7 @@ const usePikachuTemplate = (): PikachuTemplate => {
|
||||
paddingBottom: metrics.gapY(0.5),
|
||||
},
|
||||
headerIdentity: {
|
||||
textAlign: "left",
|
||||
alignItems: "flex-start",
|
||||
...r.headerIdentity,
|
||||
rowGap: metrics.gapY(0.35),
|
||||
},
|
||||
headerName: {
|
||||
@@ -248,13 +252,13 @@ const usePikachuTemplate = (): PikachuTemplate => {
|
||||
},
|
||||
headerText: { color: background },
|
||||
contactList: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(0.75),
|
||||
},
|
||||
contactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
@@ -297,5 +301,5 @@ const usePikachuTemplate = (): PikachuTemplate => {
|
||||
}),
|
||||
} satisfies PikachuStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { isRTL } from "@reactive-resume/utils/locale";
|
||||
import { useRender } from "../../context";
|
||||
import { Image, Page, StyleSheet, View } from "../../renderer";
|
||||
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
|
||||
@@ -14,6 +13,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -154,15 +154,16 @@ const Header = ({ styles }: { styles: RhyhornStyles }) => {
|
||||
};
|
||||
|
||||
const useRhyhornTemplate = (): RhyhornTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
const colors: TemplateColorRoles = { foreground, background, primary };
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const rtl = isRTL(metadata.page.locale);
|
||||
const contactGap = metrics.gapX(0.5);
|
||||
|
||||
const bodyText = {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
@@ -170,7 +171,7 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...(rtl ? { direction: "rtl" as const, textAlign: "right" as const } : {}),
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -183,7 +184,7 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: rtl ? "rtl" : "ltr",
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -192,10 +193,10 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...(rtl ? { direction: "rtl" as const, textAlign: "right" as const } : {}),
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: rtl ? "row-reverse" : "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
|
||||
@@ -210,62 +211,47 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
|
||||
// bodyText spread first so `textAlign` below isn't clobbered by bodyText.textAlign.
|
||||
...bodyText,
|
||||
width: metadata.typography.body.fontSize,
|
||||
textAlign: rtl ? "left" : "right",
|
||||
textAlign: r.listMarkerTextAlign,
|
||||
},
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: rtl ? "row-reverse" : "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: rtl ? "left" : "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
section: { flexDirection: "column", rowGap: metrics.gapY(0.25) },
|
||||
sectionHeading: {
|
||||
color: primary,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: primary,
|
||||
textAlign: rtl ? "right" : "left",
|
||||
textAlign: r.sectionHeadingTextAlign,
|
||||
},
|
||||
item: { rowGap: metrics.gapY(0.125) },
|
||||
levelContainer: { width: "100%" },
|
||||
levelItem: { borderColor: primary },
|
||||
levelItemActive: { backgroundColor: primary },
|
||||
header: { flexDirection: rtl ? "row-reverse" : "row", alignItems: "center", columnGap: metrics.gapX(0.5) },
|
||||
header: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(0.5) },
|
||||
headerTitle: { flex: 1, rowGap: metrics.gapY(0.5) },
|
||||
headerIdentity: {
|
||||
textAlign: rtl ? "right" : "left",
|
||||
alignItems: rtl ? "flex-end" : "flex-start",
|
||||
...r.headerIdentity,
|
||||
rowGap: metrics.gapY(0.35),
|
||||
},
|
||||
headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight },
|
||||
contactList: { flexDirection: rtl ? "row-reverse" : "row", flexWrap: "wrap", rowGap: metrics.gapY(0.125) },
|
||||
contactList: { flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125) },
|
||||
contactItem: {
|
||||
flexDirection: rtl ? "row-reverse" : "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
...(rtl
|
||||
? {
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: primary,
|
||||
paddingLeft: metrics.gapX(0.5),
|
||||
marginLeft: metrics.gapX(0.5),
|
||||
}
|
||||
: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: primary,
|
||||
paddingRight: metrics.gapX(0.5),
|
||||
marginRight: metrics.gapX(0.5),
|
||||
}),
|
||||
...r.contactSeparator(primary, contactGap),
|
||||
},
|
||||
contactItemContent: {
|
||||
flexDirection: rtl ? "row-reverse" : "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
contactItemLast: rtl
|
||||
? { borderLeftWidth: 0, paddingLeft: 0, marginLeft: 0 }
|
||||
: { borderRightWidth: 0, paddingRight: 0, marginRight: 0 },
|
||||
contactItemLast: r.contactSeparatorClear,
|
||||
picture: {
|
||||
width: picture.size,
|
||||
height: picture.size,
|
||||
@@ -297,5 +283,5 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
|
||||
}),
|
||||
} satisfies RhyhornStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Icon, Link, Text } from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
|
||||
@@ -103,9 +104,10 @@ const Header = ({ styles }: { styles: ScizorStyles }) => {
|
||||
};
|
||||
|
||||
const useScizorTemplate = (): ScizorTemplate => {
|
||||
const { picture, metadata } = useRender();
|
||||
const { picture, metadata, rtl } = useRender();
|
||||
|
||||
return useMemo(() => {
|
||||
const r = createRtlStyleHelpers(rtl);
|
||||
const foreground = rgbaStringToHex(metadata.design.colors.text);
|
||||
const background = rgbaStringToHex(metadata.design.colors.background);
|
||||
const primary = rgbaStringToHex(metadata.design.colors.primary);
|
||||
@@ -118,6 +120,7 @@ const useScizorTemplate = (): ScizorTemplate => {
|
||||
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
} satisfies Style;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
@@ -132,6 +135,7 @@ const useScizorTemplate = (): ScizorTemplate => {
|
||||
fontFamily: metadata.typography.body.fontFamily,
|
||||
fontSize: metadata.typography.body.fontSize,
|
||||
lineHeight: metadata.typography.body.lineHeight,
|
||||
direction: r.pageDirection,
|
||||
},
|
||||
text: bodyText,
|
||||
heading: {
|
||||
@@ -140,24 +144,25 @@ const useScizorTemplate = (): ScizorTemplate => {
|
||||
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "700",
|
||||
lineHeight: metadata.typography.heading.lineHeight,
|
||||
color: foreground,
|
||||
...r.text,
|
||||
},
|
||||
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
|
||||
link: { textDecoration: "none", color: foreground },
|
||||
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
|
||||
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "700", color: foreground },
|
||||
richParagraph: { margin: 0, ...bodyText },
|
||||
richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" },
|
||||
richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText },
|
||||
richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign },
|
||||
richListItemContent: { flex: 1, ...bodyText },
|
||||
splitRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
columnGap: metrics.gapX(2 / 3),
|
||||
},
|
||||
alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
|
||||
alignEnd: { ...r.alignEnd },
|
||||
section: {
|
||||
flexDirection: "column",
|
||||
rowGap: metrics.gapY(0.25),
|
||||
@@ -177,12 +182,12 @@ const useScizorTemplate = (): ScizorTemplate => {
|
||||
levelItem: { borderColor: primary },
|
||||
levelItemActive: { backgroundColor: primary },
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "flex-start",
|
||||
columnGap: metrics.gapX(1),
|
||||
paddingBottom: metrics.gapY(0.35),
|
||||
},
|
||||
headerIdentity: { flex: 1, alignItems: "flex-start", rowGap: metrics.gapY(0.45) },
|
||||
headerIdentity: { flex: 1, ...r.headerIdentity, rowGap: metrics.gapY(0.45) },
|
||||
headerName: {
|
||||
color: foreground,
|
||||
fontSize: metadata.typography.heading.fontSize * 1.85,
|
||||
@@ -195,13 +200,13 @@ const useScizorTemplate = (): ScizorTemplate => {
|
||||
},
|
||||
headerHeadline: { color: foreground },
|
||||
headerContactRow: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.125),
|
||||
columnGap: metrics.gapX(0.55),
|
||||
},
|
||||
headerContactItem: {
|
||||
flexDirection: "row",
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
color: foreground,
|
||||
@@ -240,5 +245,5 @@ const useScizorTemplate = (): ScizorTemplate => {
|
||||
}),
|
||||
} satisfies ScizorStyles,
|
||||
};
|
||||
}, [picture, metadata]);
|
||||
}, [picture, metadata, rtl]);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { ReactNode } from "react";
|
||||
import { createElement } from "react";
|
||||
import { Text as PdfText } from "../../renderer";
|
||||
import {
|
||||
getRichTextEdgeTrimStyle,
|
||||
isRichTextElementInsideListItem,
|
||||
stripRichTextVerticalMargins,
|
||||
} from "./rich-text-spacing";
|
||||
import { composeStyles } from "./styles";
|
||||
|
||||
export const toRichTextStyleArray = (style: Style | Style[] | undefined): Style[] => {
|
||||
if (!style) return [];
|
||||
if (Array.isArray(style)) return style.filter(Boolean);
|
||||
|
||||
return [style];
|
||||
};
|
||||
|
||||
type RichTextParagraphRendererProps = {
|
||||
children: ReactNode;
|
||||
element: Parameters<typeof isRichTextElementInsideListItem>[0];
|
||||
style: Style | Style[] | undefined;
|
||||
rtl?: boolean;
|
||||
rtlTextWrapStyle?: Style | undefined;
|
||||
applyRtlDirection?: (node: ReactNode) => ReactNode;
|
||||
};
|
||||
|
||||
export const renderRichTextParagraph = ({
|
||||
element,
|
||||
style,
|
||||
children,
|
||||
rtl,
|
||||
rtlTextWrapStyle,
|
||||
applyRtlDirection,
|
||||
}: RichTextParagraphRendererProps) => {
|
||||
const paragraphStyles = isRichTextElementInsideListItem(element)
|
||||
? toRichTextStyleArray(style).map(stripRichTextVerticalMargins)
|
||||
: style;
|
||||
|
||||
const composedStyle = composeStyles(
|
||||
paragraphStyles,
|
||||
getRichTextEdgeTrimStyle(element),
|
||||
rtl ? rtlTextWrapStyle : undefined,
|
||||
);
|
||||
|
||||
const content = rtl && applyRtlDirection ? applyRtlDirection(children) : children;
|
||||
|
||||
return createElement(PdfText, { style: composedStyle }, content);
|
||||
};
|
||||
@@ -1,5 +1,14 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Text as PdfText } from "@react-pdf/renderer";
|
||||
import { parse } from "node-html-parser";
|
||||
import { createElement } from "react";
|
||||
import { normalizeRichTextHtml } from "./rich-text-html";
|
||||
import { renderRichTextParagraph } from "./rich-text-renderers";
|
||||
|
||||
type PdfElement = ReactElement<{ children?: unknown; style?: unknown }>;
|
||||
|
||||
const getPdfElementProps = (element: unknown) => (element as PdfElement).props;
|
||||
|
||||
describe("normalizeRichTextHtml", () => {
|
||||
it("wraps top-level inline rich text in a paragraph", () => {
|
||||
@@ -23,3 +32,21 @@ describe("normalizeRichTextHtml", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderRichTextParagraph", () => {
|
||||
it("keeps unmarked paragraph text inside a PDF text node", () => {
|
||||
const paragraph = parse("<p>Plain <strong>bold</strong> text</p>").querySelector("p");
|
||||
|
||||
if (!paragraph) throw new Error("Expected paragraph to exist.");
|
||||
|
||||
const rendered = renderRichTextParagraph({
|
||||
element: paragraph,
|
||||
style: { fontSize: 10 },
|
||||
children: ["Plain ", createElement(PdfText, { key: "bold" }, "bold"), " text"],
|
||||
});
|
||||
const props = getPdfElementProps(rendered);
|
||||
|
||||
expect(rendered.type).toBe(PdfText);
|
||||
expect(props.children).toEqual(["Plain ", expect.any(Object), " text"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,16 +2,15 @@ import type { Style } from "@react-pdf/types";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
import { cloneElement, isValidElement } from "react";
|
||||
import { Html } from "react-pdf-html";
|
||||
import { isRTL } from "@reactive-resume/utils/locale";
|
||||
import { useRender } from "../../context";
|
||||
import { Text as PdfText, View } from "../../renderer";
|
||||
import { useTemplateStyle } from "./context";
|
||||
import { safeTextStyle } from "./primitives";
|
||||
import { convertPseudoBulletParagraphs, normalizeRichTextHtml, richTextMarkClassName } from "./rich-text-html";
|
||||
import { renderRichTextParagraph, toRichTextStyleArray } from "./rich-text-renderers";
|
||||
import {
|
||||
createRichTextProseSpacing,
|
||||
getRichTextEdgeTrimStyle,
|
||||
isRichTextElementInsideListItem,
|
||||
isRichTextElementInsideOrderedList,
|
||||
resolveRichTextBodyLineHeight,
|
||||
stripRichTextVerticalMargins,
|
||||
@@ -26,13 +25,6 @@ const richMarkStyle = {
|
||||
backgroundColor: "#ffff00",
|
||||
} satisfies Style;
|
||||
|
||||
const toStyleArray = (style: Style | Style[] | undefined): Style[] => {
|
||||
if (!style) return [];
|
||||
if (Array.isArray(style)) return style.filter(Boolean);
|
||||
|
||||
return [style];
|
||||
};
|
||||
|
||||
// react-pdf textkit reads BiDi base direction from each run's own `direction` attribute
|
||||
// (default "ltr"), and react-pdf-html buckets inline content into styleless inner <Text>
|
||||
// frames — so the rtl style has to be injected onto every descendant, not just a wrapper.
|
||||
@@ -65,8 +57,7 @@ const applyRtlDirectionRecursively = (node: ReactNode): ReactNode => {
|
||||
};
|
||||
|
||||
export const RichText = ({ children }: { children: string }) => {
|
||||
const data = useRender();
|
||||
const rtl = isRTL(data.metadata.page.locale);
|
||||
const { rtl } = useRender();
|
||||
const rtlTextWrapStyle: Style | undefined = rtl ? { direction: "rtl", textAlign: "right" } : undefined;
|
||||
const boldStyle = useTemplateStyle("bold");
|
||||
const linkStyle = useTemplateStyle("link");
|
||||
@@ -95,25 +86,20 @@ export const RichText = ({ children }: { children: string }) => {
|
||||
resetStyles
|
||||
renderers={{
|
||||
b: ({ children }) => <PdfText style={composeStyles(boldStyle, safeTextStyle)}>{children}</PdfText>,
|
||||
p: ({ element, style, children }) => {
|
||||
const paragraphStyles = isRichTextElementInsideListItem(element)
|
||||
? toStyleArray(style).map(stripRichTextVerticalMargins)
|
||||
: style;
|
||||
p: (props) => {
|
||||
const paragraphProps = {
|
||||
...props,
|
||||
rtl,
|
||||
...(rtlTextWrapStyle ? { rtlTextWrapStyle } : {}),
|
||||
...(rtl ? { applyRtlDirection: applyRtlDirectionRecursively } : {}),
|
||||
};
|
||||
|
||||
if (rtl) {
|
||||
return (
|
||||
<PdfText style={composeStyles(paragraphStyles, getRichTextEdgeTrimStyle(element), rtlTextWrapStyle)}>
|
||||
{applyRtlDirectionRecursively(children)}
|
||||
</PdfText>
|
||||
);
|
||||
}
|
||||
|
||||
return <View style={composeStyles(paragraphStyles, getRichTextEdgeTrimStyle(element))}>{children}</View>;
|
||||
return renderRichTextParagraph(paragraphProps);
|
||||
},
|
||||
li: ({ element, style, children }) => {
|
||||
const isOrderedList = isRichTextElementInsideOrderedList(element);
|
||||
const marker = isOrderedList ? `${element.indexOfType + 1}.` : "•";
|
||||
const itemStyles = toStyleArray(style);
|
||||
const itemStyles = toRichTextStyleArray(style);
|
||||
const contentItemStyles = itemStyles.map(stripRichTextVerticalMargins);
|
||||
|
||||
const markerNode = (
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRtlStyleHelpers } from "./rtl";
|
||||
|
||||
describe("createRtlStyleHelpers", () => {
|
||||
it("returns LTR defaults when rtl is false", () => {
|
||||
const r = createRtlStyleHelpers(false);
|
||||
|
||||
expect(r.pageDirection).toBe("ltr");
|
||||
expect(r.row).toBe("row");
|
||||
expect(r.text).toEqual({});
|
||||
expect(r.alignEnd.textAlign).toBe("right");
|
||||
expect(r.gridRowStyle).toBeUndefined();
|
||||
expect(r.anchorToStart()).toEqual({ left: 0, right: undefined });
|
||||
});
|
||||
|
||||
it("returns RTL mirrors when rtl is true", () => {
|
||||
const r = createRtlStyleHelpers(true);
|
||||
|
||||
expect(r.pageDirection).toBe("rtl");
|
||||
expect(r.row).toBe("row-reverse");
|
||||
expect(r.text).toEqual({ direction: "rtl", textAlign: "right" });
|
||||
expect(r.alignEnd.textAlign).toBe("left");
|
||||
expect(r.gridRowStyle).toEqual({ flexDirection: "row-reverse" });
|
||||
expect(r.anchorToStart()).toEqual({ right: 0, left: undefined });
|
||||
});
|
||||
|
||||
it("mirrors contact separator borders", () => {
|
||||
const ltr = createRtlStyleHelpers(false).contactSeparator("#000", 4);
|
||||
const rtl = createRtlStyleHelpers(true).contactSeparator("#000", 4);
|
||||
|
||||
expect(ltr).toMatchObject({ borderRightWidth: 1, paddingRight: 4, marginRight: 4 });
|
||||
expect(rtl).toMatchObject({ borderLeftWidth: 1, paddingLeft: 4, marginLeft: 4 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
|
||||
export type RtlStyleHelpers = {
|
||||
rtl: boolean;
|
||||
pageDirection: "ltr" | "rtl";
|
||||
row: "row" | "row-reverse";
|
||||
text: Pick<Style, "direction" | "textAlign">;
|
||||
alignEnd: Pick<Style, "textAlign" | "minWidth" | "maxWidth" | "flexShrink">;
|
||||
sectionHeadingTextAlign: NonNullable<Style["textAlign"]>;
|
||||
headerIdentity: Pick<Style, "textAlign" | "alignItems">;
|
||||
listMarkerTextAlign: NonNullable<Style["textAlign"]>;
|
||||
gridRowStyle: Style | undefined;
|
||||
contactSeparator: (color: string, gap: number) => Style;
|
||||
contactSeparatorClear: Style;
|
||||
anchorToStart: (offset?: number | string) => Style;
|
||||
};
|
||||
|
||||
export function createRtlStyleHelpers(rtl: boolean): RtlStyleHelpers {
|
||||
return {
|
||||
rtl,
|
||||
pageDirection: rtl ? "rtl" : "ltr",
|
||||
row: rtl ? "row-reverse" : "row",
|
||||
text: rtl ? { direction: "rtl", textAlign: "right" } : {},
|
||||
alignEnd: {
|
||||
textAlign: rtl ? "left" : "right",
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
flexShrink: 1,
|
||||
},
|
||||
sectionHeadingTextAlign: rtl ? "right" : "left",
|
||||
headerIdentity: rtl
|
||||
? { textAlign: "right", alignItems: "flex-end" }
|
||||
: { textAlign: "left", alignItems: "flex-start" },
|
||||
listMarkerTextAlign: rtl ? "left" : "right",
|
||||
gridRowStyle: rtl ? { flexDirection: "row-reverse" } : undefined,
|
||||
contactSeparator: (color, gap) =>
|
||||
rtl
|
||||
? {
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: color,
|
||||
paddingLeft: gap,
|
||||
marginLeft: gap,
|
||||
}
|
||||
: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: color,
|
||||
paddingRight: gap,
|
||||
marginRight: gap,
|
||||
},
|
||||
contactSeparatorClear: rtl
|
||||
? { borderLeftWidth: 0, paddingLeft: 0, marginLeft: 0 }
|
||||
: { borderRightWidth: 0, paddingRight: 0, marginRight: 0 },
|
||||
anchorToStart: (offset = 0) => (rtl ? { right: offset } : { left: offset }),
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import type { StyleInput, TemplatePlacement } from "./styles";
|
||||
import type { CustomItemSection, ItemSection } from "./types";
|
||||
import { Children, createContext, isValidElement, use } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { isRTL } from "@reactive-resume/utils/locale";
|
||||
import { useRender } from "../../context";
|
||||
import { View } from "../../renderer";
|
||||
import { getResumeSectionTitle } from "../../section-title";
|
||||
@@ -39,6 +38,7 @@ import { LevelDisplay } from "./level-display";
|
||||
import { getTemplateMetrics } from "./metrics";
|
||||
import { Bold, Div, Heading, Icon, Link, Small, Text } from "./primitives";
|
||||
import { RichText } from "./rich-text";
|
||||
import { createRtlStyleHelpers } from "./rtl";
|
||||
import { getInlineItemWebsiteUrl, shouldRenderSeparateItemWebsite } from "./section-links";
|
||||
import { hasSplitRowText, promoteSplitRowRight } from "./split-row";
|
||||
import { composeStyles } from "./styles";
|
||||
@@ -118,8 +118,7 @@ const SectionItems = ({ children, columns = 1 }: { children: ReactNode; columns?
|
||||
columns: layout.columns,
|
||||
});
|
||||
const context = { itemStyle: layout.itemStyle, useTimeline };
|
||||
const rtl = isRTL(data.metadata.page.locale);
|
||||
const rtlRowStyle = rtl ? { flexDirection: "row-reverse" as const } : undefined;
|
||||
const rtlRowStyle = createRtlStyleHelpers(data.rtl).gridRowStyle;
|
||||
|
||||
if (!useTimeline) {
|
||||
if (layout.isGrid) {
|
||||
@@ -340,7 +339,7 @@ const ExperienceSection = ({
|
||||
const experience = sectionData ?? data.sections.experience;
|
||||
const items = getVisibleItems(experience, "experience");
|
||||
const splitRowStyle = useSectionSplitRowStyle();
|
||||
const alignRightStyle = useTemplateStyle("alignRight");
|
||||
const alignEndStyle = useTemplateStyle("alignEnd");
|
||||
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
|
||||
|
||||
if (items.length === 0) return null;
|
||||
@@ -368,7 +367,7 @@ const ExperienceSection = ({
|
||||
) : null
|
||||
}
|
||||
middle={<ItemTitle website={item.website}>{item.company}</ItemTitle>}
|
||||
trailing={<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>}
|
||||
trailing={<Text style={composeStyles(alignEndStyle)}>{item.period}</Text>}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -376,15 +375,13 @@ const ExperienceSection = ({
|
||||
<>
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
<ItemTitle website={item.website}>{item.company}</ItemTitle>
|
||||
{hasSplitRowText(headerLocation) && (
|
||||
<Text style={composeStyles(alignRightStyle)}>{headerLocation}</Text>
|
||||
)}
|
||||
{hasSplitRowText(headerLocation) && <Text style={composeStyles(alignEndStyle)}>{headerLocation}</Text>}
|
||||
</View>
|
||||
|
||||
{item.roles.length === 0 && (hasPosition || hasSplitRowText(headerPeriod)) && (
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
{hasPosition && <Text>{item.position}</Text>}
|
||||
{hasSplitRowText(headerPeriod) && <Text style={composeStyles(alignRightStyle)}>{headerPeriod}</Text>}
|
||||
{hasSplitRowText(headerPeriod) && <Text style={composeStyles(alignEndStyle)}>{headerPeriod}</Text>}
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
@@ -400,7 +397,7 @@ const ExperienceSection = ({
|
||||
<View key={role.id}>
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
<Text>{role.position}</Text>
|
||||
<Text style={composeStyles(alignRightStyle)}>{role.period}</Text>
|
||||
<Text style={composeStyles(alignEndStyle)}>{role.period}</Text>
|
||||
</View>
|
||||
<RichText>{role.description}</RichText>
|
||||
</View>
|
||||
@@ -428,7 +425,7 @@ const EducationSection = ({
|
||||
const education = sectionData ?? data.sections.education;
|
||||
const items = getVisibleItems(education, "education");
|
||||
const splitRowStyle = useSectionSplitRowStyle();
|
||||
const alignRightStyle = useTemplateStyle("alignRight");
|
||||
const alignEndStyle = useTemplateStyle("alignEnd");
|
||||
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
|
||||
|
||||
if (items.length === 0) return null;
|
||||
@@ -460,7 +457,7 @@ const EducationSection = ({
|
||||
) : null
|
||||
}
|
||||
middle={<ItemTitle website={item.website}>{item.school}</ItemTitle>}
|
||||
trailing={<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>}
|
||||
trailing={<Text style={composeStyles(alignEndStyle)}>{item.period}</Text>}
|
||||
/>
|
||||
{gradeAndLocation && <Text>{gradeAndLocation}</Text>}
|
||||
</>
|
||||
@@ -471,7 +468,7 @@ const EducationSection = ({
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
<ItemTitle website={item.website}>{item.school}</ItemTitle>
|
||||
{hasSplitRowText(headerDegreeAndGrade) && (
|
||||
<Text style={composeStyles(alignRightStyle)}>{headerDegreeAndGrade}</Text>
|
||||
<Text style={composeStyles(alignEndStyle)}>{headerDegreeAndGrade}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -479,7 +476,7 @@ const EducationSection = ({
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
{hasArea && <Text>{item.area}</Text>}
|
||||
{hasSplitRowText(headerLocationAndPeriod) && (
|
||||
<Text style={composeStyles(alignRightStyle)}>{headerLocationAndPeriod}</Text>
|
||||
<Text style={composeStyles(alignEndStyle)}>{headerLocationAndPeriod}</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
@@ -512,7 +509,7 @@ const ProjectsSection = ({
|
||||
const projects = sectionData ?? data.sections.projects;
|
||||
const items = getVisibleItems(projects, "projects");
|
||||
const splitRowStyle = useSectionSplitRowStyle();
|
||||
const alignRightStyle = useTemplateStyle("alignRight");
|
||||
const alignEndStyle = useTemplateStyle("alignEnd");
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
@@ -524,7 +521,7 @@ const ProjectsSection = ({
|
||||
<SectionItemHeader>
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
<ItemTitle website={item.website}>{item.name}</ItemTitle>
|
||||
<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>
|
||||
<Text style={composeStyles(alignEndStyle)}>{item.period}</Text>
|
||||
</View>
|
||||
</SectionItemHeader>
|
||||
|
||||
@@ -653,7 +650,7 @@ const AwardsSection = ({
|
||||
const awards = sectionData ?? data.sections.awards;
|
||||
const items = getVisibleItems(awards, "awards");
|
||||
const splitRowStyle = useTemplateStyle("splitRow");
|
||||
const alignRightStyle = useTemplateStyle("alignRight");
|
||||
const alignEndStyle = useTemplateStyle("alignEnd");
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
@@ -665,7 +662,7 @@ const AwardsSection = ({
|
||||
<SectionItemHeader>
|
||||
<View style={composeStyles(splitRowStyle, awardTitleDateRowStyle)}>
|
||||
<ItemTitle website={item.website}>{item.title}</ItemTitle>
|
||||
<Text style={composeStyles(alignRightStyle)}>{item.date}</Text>
|
||||
<Text style={composeStyles(alignEndStyle)}>{item.date}</Text>
|
||||
</View>
|
||||
<Text>{item.awarder}</Text>
|
||||
</SectionItemHeader>
|
||||
@@ -690,7 +687,7 @@ const CertificationsSection = ({
|
||||
const certifications = sectionData ?? data.sections.certifications;
|
||||
const items = getVisibleItems(certifications, "certifications");
|
||||
const splitRowStyle = useSectionSplitRowStyle();
|
||||
const alignRightStyle = useTemplateStyle("alignRight");
|
||||
const alignEndStyle = useTemplateStyle("alignEnd");
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
@@ -702,7 +699,7 @@ const CertificationsSection = ({
|
||||
<SectionItemHeader>
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
<ItemTitle website={item.website}>{item.title}</ItemTitle>
|
||||
<Text style={composeStyles(alignRightStyle)}>{item.date}</Text>
|
||||
<Text style={composeStyles(alignEndStyle)}>{item.date}</Text>
|
||||
</View>
|
||||
<Text>{item.issuer}</Text>
|
||||
</SectionItemHeader>
|
||||
@@ -728,7 +725,7 @@ const PublicationsSection = ({
|
||||
const publications = sectionData ?? data.sections.publications;
|
||||
const items = getVisibleItems(publications, "publications");
|
||||
const splitRowStyle = useSectionSplitRowStyle();
|
||||
const alignRightStyle = useTemplateStyle("alignRight");
|
||||
const alignEndStyle = useTemplateStyle("alignEnd");
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
@@ -740,7 +737,7 @@ const PublicationsSection = ({
|
||||
<SectionItemHeader>
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
<ItemTitle website={item.website}>{item.title}</ItemTitle>
|
||||
<Text style={composeStyles(alignRightStyle)}>{item.date}</Text>
|
||||
<Text style={composeStyles(alignEndStyle)}>{item.date}</Text>
|
||||
</View>
|
||||
|
||||
<Text>{item.publisher}</Text>
|
||||
@@ -767,7 +764,7 @@ const VolunteerSection = ({
|
||||
const volunteer = sectionData ?? data.sections.volunteer;
|
||||
const items = getVisibleItems(volunteer, "volunteer");
|
||||
const splitRowStyle = useSectionSplitRowStyle();
|
||||
const alignRightStyle = useTemplateStyle("alignRight");
|
||||
const alignEndStyle = useTemplateStyle("alignEnd");
|
||||
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
|
||||
|
||||
if (items.length === 0) return null;
|
||||
@@ -783,15 +780,13 @@ const VolunteerSection = ({
|
||||
<InlineItemHeader
|
||||
leading={hasSplitRowText(item.location) ? <Text>{item.location}</Text> : null}
|
||||
middle={<ItemTitle website={item.website}>{item.organization}</ItemTitle>}
|
||||
trailing={<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>}
|
||||
trailing={<Text style={composeStyles(alignEndStyle)}>{item.period}</Text>}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<View style={composeStyles(splitRowStyle)}>
|
||||
<ItemTitle website={item.website}>{item.organization}</ItemTitle>
|
||||
{hasSplitRowText(item.period) && (
|
||||
<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>
|
||||
)}
|
||||
{hasSplitRowText(item.period) && <Text style={composeStyles(alignEndStyle)}>{item.period}</Text>}
|
||||
</View>
|
||||
|
||||
<Text>{item.location}</Text>
|
||||
|
||||
@@ -58,7 +58,7 @@ export type TemplateStyleSlots = {
|
||||
richListItemMarker?: TemplateStyleSlot;
|
||||
richListItemContent?: TemplateStyleSlot;
|
||||
splitRow?: TemplateStyleSlot;
|
||||
alignRight?: TemplateStyleSlot;
|
||||
alignEnd?: TemplateStyleSlot;
|
||||
inlineItemHeader?: TemplateStyleSlot;
|
||||
inlineItemHeaderLeading?: TemplateStyleSlot;
|
||||
inlineItemHeaderMiddle?: TemplateStyleSlot;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultLocale, isLocale } from "./locale";
|
||||
import { defaultLocale, isLocale, isRTL } from "./locale";
|
||||
|
||||
describe("defaultLocale", () => {
|
||||
it("is en-US", () => {
|
||||
@@ -40,3 +40,16 @@ describe("isLocale", () => {
|
||||
expect(isLocale([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRTL", () => {
|
||||
it.each([
|
||||
["ar-SA", true],
|
||||
["he-IL", true],
|
||||
["fa-IR", true],
|
||||
["en-US", false],
|
||||
["en-GB", false],
|
||||
["fr-FR", false],
|
||||
])("returns %s → %s", (locale, expected) => {
|
||||
expect(isRTL(locale)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,6 +84,6 @@ const RTL_LANGUAGES = new Set([
|
||||
]);
|
||||
|
||||
export function isRTL(locale: string): boolean {
|
||||
const language = locale.split("-")[0].toLowerCase();
|
||||
const language = locale.split("-")[0]?.toLowerCase() ?? "";
|
||||
return RTL_LANGUAGES.has(language);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user