mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
feat: add semantic CSS stylesheets (#3274)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor Agent
parent
4ac19f81b3
commit
d2ffbf9618
@@ -9,8 +9,10 @@ import type {
|
||||
} from "../shared/types";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -23,7 +25,14 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
@@ -56,21 +65,34 @@ const azurillFeatures = {
|
||||
sectionTimeline: true,
|
||||
} satisfies TemplateFeatures;
|
||||
|
||||
export const AzurillPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const AzurillPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles, featureStyles } = useAzurillTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} featureStyles={featureStyles} colors={colors} features={azurillFeatures}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider
|
||||
pageNodeKey={pageNodeKey}
|
||||
styles={styles}
|
||||
featureStyles={featureStyles}
|
||||
colors={colors}
|
||||
features={azurillFeatures}
|
||||
>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.contentRow, { columnGap: metrics.columnGap })}>
|
||||
<View
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sidebarColumn, {
|
||||
flexBasis: `${metadata.layout.sidebarWidth}%`,
|
||||
display: page.fullWidth ? "none" : "flex",
|
||||
@@ -82,13 +104,13 @@ export const AzurillPage = ({ page, pageSize, pageMinHeightStyle, showHeader }:
|
||||
<Section section={section} placement="sidebar" />
|
||||
</Fragment>
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</View>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -100,8 +122,8 @@ const Header = ({ styles }: AzurillHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -110,7 +132,7 @@ const Header = ({ styles }: AzurillHeaderProps) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerContactRow}>
|
||||
<SemanticContactListView style={styles.headerContactRow}>
|
||||
<EmailContactItem email={basics.email} style={styles.headerContactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.headerContactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.headerContactItem} />
|
||||
@@ -118,8 +140,8 @@ const Header = ({ styles }: AzurillHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.headerContactItem} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const azurillSemanticManifest = {
|
||||
template: "azurill",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "timeline-line",
|
||||
key: "timeline-line",
|
||||
owner: { kind: "section-items", key: "section-items", placement: "main", columns: 1 },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "start" },
|
||||
},
|
||||
{
|
||||
name: "timeline-marker",
|
||||
key: "timeline-marker",
|
||||
owner: { kind: "item", key: "item", placement: "main", columns: 1 },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "start" },
|
||||
},
|
||||
{
|
||||
name: "timeline-dot",
|
||||
key: "timeline-dot",
|
||||
owner: { kind: "item", key: "item", placement: "main", columns: 1 },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "timeline-marker", at: "start" },
|
||||
},
|
||||
{
|
||||
name: "timeline-content",
|
||||
key: "timeline-content",
|
||||
owner: { kind: "item", key: "item", placement: "main", columns: 1 },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "end", take: "all" },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleSlots } from "../shared/types";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,14 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -68,25 +77,31 @@ const getBronzorSections = ({
|
||||
return sections;
|
||||
};
|
||||
|
||||
export const BronzorPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const BronzorPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { styles, colors } = useBronzorTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sections = getBronzorSections({ mainSections, sidebarSections, fullWidth: page.fullWidth });
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sections, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sections, { rowGap: metrics.sectionGap })}>
|
||||
{sections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
);
|
||||
@@ -97,8 +112,8 @@ const Header = ({ styles }: BronzorHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -107,7 +122,7 @@ const Header = ({ styles }: BronzorHeaderProps) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerContactRow}>
|
||||
<SemanticContactListView style={styles.headerContactRow}>
|
||||
<EmailContactItem email={basics.email} style={styles.headerContactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.headerContactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.headerContactItem} />
|
||||
@@ -115,8 +130,8 @@ const Header = ({ styles }: BronzorHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.headerContactItem} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const bronzorSemanticManifest = {
|
||||
template: "bronzor",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["sidebar", "main"], flow: "interleaved" },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "interleaved-section-row",
|
||||
key: "interleaved-section-row",
|
||||
owner: { kind: "section", key: "section", placement: "main" },
|
||||
binding: {
|
||||
type: "alias",
|
||||
canonicalKind: "section",
|
||||
token: "interleaved-section-row",
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,15 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticContactRowView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
@@ -45,19 +55,26 @@ type ChikoritaHeaderProps = {
|
||||
styles: ChikoritaStyles;
|
||||
};
|
||||
|
||||
export const ChikoritaPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const ChikoritaPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata, picture } = data;
|
||||
const { colors, styles } = useChikoritaTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<View
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
<SemanticRegionView
|
||||
region="main"
|
||||
style={composeStyles(styles.mainColumn, {
|
||||
paddingTop: metrics.page.paddingVertical,
|
||||
paddingRight: page.fullWidth ? metrics.page.paddingHorizontal : metrics.columnGap,
|
||||
@@ -71,9 +88,10 @@ export const ChikoritaPage = ({ page, pageSize, pageMinHeightStyle, showHeader }
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
<View
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sidebarColumn, {
|
||||
display: page.fullWidth ? "none" : "flex",
|
||||
flexBasis: `${metadata.layout.sidebarWidth}%`,
|
||||
@@ -92,7 +110,7 @@ export const ChikoritaPage = ({ page, pageSize, pageMinHeightStyle, showHeader }
|
||||
<Section section={section} placement="sidebar" />
|
||||
</Fragment>
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
);
|
||||
@@ -103,8 +121,8 @@ const Header = ({ styles }: ChikoritaHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -112,22 +130,22 @@ const Header = ({ styles }: ChikoritaHeaderProps) => {
|
||||
<Text>{basics.headline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerContactList}>
|
||||
<View style={styles.headerContactRow}>
|
||||
<SemanticContactListView style={styles.headerContactList}>
|
||||
<SemanticContactRowView partKey="contact-row-primary" style={styles.headerContactRow}>
|
||||
<EmailContactItem email={basics.email} style={styles.headerContactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.headerContactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.headerContactItem} />
|
||||
</View>
|
||||
</SemanticContactRowView>
|
||||
|
||||
<View style={styles.headerContactRow}>
|
||||
<SemanticContactRowView partKey="contact-row-secondary" style={styles.headerContactRow}>
|
||||
<WebsiteContactItem website={basics.website} style={styles.headerContactItem} />
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.headerContactItem} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SemanticContactRowView>
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const chikoritaSemanticManifest = {
|
||||
template: "chikorita",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "contact-row-primary",
|
||||
key: "contact-row-primary",
|
||||
owner: { kind: "contact-list", key: "contact-list" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: {
|
||||
parent: "owner",
|
||||
at: "start",
|
||||
take: [
|
||||
{ kind: "contact-item", name: "email" },
|
||||
{ kind: "contact-item", name: "phone" },
|
||||
{ kind: "contact-item", name: "location" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "contact-row-secondary",
|
||||
key: "contact-row-secondary",
|
||||
owner: { kind: "contact-list", key: "contact-list" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: {
|
||||
parent: "owner",
|
||||
at: "end",
|
||||
take: [
|
||||
{ kind: "contact-item", name: "website" },
|
||||
{ kind: "contact-item", name: "custom" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -35,13 +35,15 @@ describe("cover letter PDF layout", () => {
|
||||
expect(source).toContain("showHeader={shouldShowResumeHeader");
|
||||
expect(source).toContain("pageSize={pageSize}");
|
||||
expect(source).toContain("pageMinHeightStyle={pageMinHeightStyle}");
|
||||
expect(source).toContain("pageNumber={index + 1}");
|
||||
});
|
||||
|
||||
it.each(pageFiles)("%s renders the shared page props", (file) => {
|
||||
const source = readTemplate(file);
|
||||
|
||||
expect(source, basename(file)).toContain("pageSize, pageMinHeightStyle, showHeader");
|
||||
expect(source, basename(file)).toContain("<Page size={pageSize}");
|
||||
expect(source, basename(file)).toContain("pageSize, pageMinHeightStyle, showHeader, pageNumber");
|
||||
expect(source, basename(file)).toContain("size={semanticPageSize ?? pageSize}");
|
||||
expect(source, basename(file)).toContain("pageNodeKey={pageNodeKey}");
|
||||
expect(source, basename(file)).toContain("showHeader &&");
|
||||
expect(source, basename(file)).not.toContain('from "../shared/cover-letter"');
|
||||
expect(source, basename(file)).not.toContain('from "../shared/page-size"');
|
||||
|
||||
@@ -3,8 +3,10 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import { getPrimaryTint } from "../shared/color-helpers";
|
||||
import {
|
||||
@@ -19,7 +21,15 @@ import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionTemplatePartView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
@@ -56,14 +66,16 @@ const ditgarFeatures = {
|
||||
mainItemHeaderBorder: true,
|
||||
} satisfies TemplateFeatures;
|
||||
|
||||
export const DitgarPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const DitgarPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useDitgarTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const showSidebar = !page.fullWidth || showHeader;
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const { featuredSummarySection, regularSections: regularMainSections } = getFeaturedSummaryLayout({
|
||||
sections: mainSections,
|
||||
canFeatureSummary: showHeader,
|
||||
@@ -73,8 +85,12 @@ export const DitgarPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: T
|
||||
: sidebarSections;
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors} features={ditgarFeatures}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors} features={ditgarFeatures}>
|
||||
{showSidebar && (
|
||||
<View
|
||||
style={composeStyles(styles.sidebarColumn, {
|
||||
@@ -84,27 +100,34 @@ export const DitgarPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: T
|
||||
{showHeader && <Header styles={styles} colors={colors} />}
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}
|
||||
>
|
||||
{regularSidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.mainColumn}>
|
||||
{featuredSummarySection && (
|
||||
<View style={styles.specialContainer}>
|
||||
<SemanticRegionTemplatePartView
|
||||
region="featured"
|
||||
partKeys={["featured-summary"]}
|
||||
style={styles.specialContainer}
|
||||
>
|
||||
<Section section={featuredSummarySection} placement="main" showHeading={false} />
|
||||
</View>
|
||||
</SemanticRegionTemplatePartView>
|
||||
)}
|
||||
|
||||
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
{regularMainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</View>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -116,8 +139,8 @@ const Header = ({ styles, colors }: DitgarHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -126,7 +149,7 @@ const Header = ({ styles, colors }: DitgarHeaderProps) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem
|
||||
email={basics.email}
|
||||
style={styles.contactItem}
|
||||
@@ -161,8 +184,8 @@ const Header = ({ styles, colors }: DitgarHeaderProps) => {
|
||||
iconColor={colors.background}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
const borderedItemHeaderSectionTypes = [
|
||||
"profiles",
|
||||
"experience",
|
||||
"education",
|
||||
"projects",
|
||||
"skills",
|
||||
"languages",
|
||||
"interests",
|
||||
"awards",
|
||||
"certifications",
|
||||
"publications",
|
||||
"volunteer",
|
||||
"references",
|
||||
] as const;
|
||||
|
||||
export const ditgarSemanticManifest = {
|
||||
template: "ditgar",
|
||||
regions: [
|
||||
{ name: "header", placement: "sidebar", origins: [] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
{ name: "featured", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
],
|
||||
header: { region: "header", placement: "sidebar" },
|
||||
specialSummary: { region: "featured", placement: "main", source: "main-with-header" },
|
||||
parts: [
|
||||
{
|
||||
name: "featured-summary",
|
||||
key: "featured-summary",
|
||||
owner: { kind: "region", key: "featured" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "start", take: "all" },
|
||||
},
|
||||
{
|
||||
name: "sidebar-background",
|
||||
key: "sidebar-background",
|
||||
owner: { kind: "region", key: "sidebar" },
|
||||
binding: { type: "alias", canonicalKind: "region", token: "sidebar-background" },
|
||||
},
|
||||
{
|
||||
name: "item-header-border",
|
||||
key: "item-header-border",
|
||||
owner: {
|
||||
kind: "item-header",
|
||||
key: "item-header",
|
||||
sectionTypes: borderedItemHeaderSectionTypes,
|
||||
},
|
||||
binding: { type: "alias", canonicalKind: "item-header", token: "item-header-border" },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,15 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
SemanticTemplatePartView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -50,22 +60,29 @@ type DittoHeaderProps = {
|
||||
styles: DittoStyles;
|
||||
};
|
||||
|
||||
export const DittoPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const DittoPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata, picture } = data;
|
||||
const { colors, styles } = useDittoTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.contentRow, { paddingTop: metrics.headerGap })}>
|
||||
<View
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sidebarColumn, {
|
||||
display: page.fullWidth ? "none" : "flex",
|
||||
width: `${metadata.layout.sidebarWidth}%`,
|
||||
@@ -79,9 +96,10 @@ export const DittoPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: Te
|
||||
<Section section={section} placement="sidebar" />
|
||||
</Fragment>
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
<View
|
||||
<SemanticRegionView
|
||||
region="main"
|
||||
style={composeStyles(styles.mainColumn, {
|
||||
paddingLeft: metrics.columnGap,
|
||||
paddingRight: metrics.page.paddingHorizontal,
|
||||
@@ -91,7 +109,7 @@ export const DittoPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: Te
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</View>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -103,9 +121,11 @@ const Header = ({ styles }: DittoHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerBand}>
|
||||
<View style={styles.pictureAnchor}>{hasPicture && <Image src={picture.url} style={styles.picture} />}</View>
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
<SemanticTemplatePartView partKeys={["header-band"]} style={styles.headerBand}>
|
||||
<SemanticTemplatePartView partKeys={["header-band", "picture-anchor"]} style={styles.pictureAnchor}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
</SemanticTemplatePartView>
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -113,12 +133,12 @@ const Header = ({ styles }: DittoHeaderProps) => {
|
||||
<Text style={styles.headerHeadline}>{basics.headline}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticTemplatePartView>
|
||||
|
||||
<View style={styles.contactRow}>
|
||||
<View style={styles.contactOffset} />
|
||||
<SemanticTemplatePartView partKeys={["contact-offset"]} style={styles.contactOffset} />
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} />
|
||||
@@ -126,9 +146,9 @@ const Header = ({ styles }: DittoHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
))}
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const dittoSemanticManifest = {
|
||||
template: "ditto",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "header-band",
|
||||
key: "header-band",
|
||||
owner: { kind: "header", key: "header" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: {
|
||||
parent: "owner",
|
||||
at: "start",
|
||||
take: [{ kind: "picture" }, { kind: "name" }, { kind: "headline" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "picture-anchor",
|
||||
key: "picture-anchor",
|
||||
owner: { kind: "header", key: "header" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "header-band", at: "start", take: [{ kind: "picture" }] },
|
||||
},
|
||||
{
|
||||
name: "contact-offset",
|
||||
key: "contact-offset",
|
||||
owner: { kind: "header", key: "header" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: { before: { kind: "contact-list" } } },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import { getPrimaryTint } from "../shared/color-helpers";
|
||||
import {
|
||||
@@ -19,7 +21,15 @@ import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionTemplatePartView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
@@ -51,14 +61,16 @@ type GengarHeaderProps = {
|
||||
colors: TemplateColorRoles;
|
||||
};
|
||||
|
||||
export const GengarPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const GengarPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useGengarTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const showSidebar = !page.fullWidth || showHeader;
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const { featuredSummarySection, regularSections: regularMainSections } = getFeaturedSummaryLayout({
|
||||
sections: mainSections,
|
||||
canFeatureSummary: showHeader,
|
||||
@@ -68,8 +80,12 @@ export const GengarPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: T
|
||||
: sidebarSections;
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showSidebar && (
|
||||
<View
|
||||
style={composeStyles(styles.sidebarColumn, {
|
||||
@@ -79,29 +95,33 @@ export const GengarPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: T
|
||||
{showHeader && <Header styles={styles} colors={colors} />}
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={styles.sidebarContent}>
|
||||
<SemanticRegionView region="sidebar" style={styles.sidebarContent}>
|
||||
{regularSidebarSections.map((section) => (
|
||||
<Fragment key={section}>
|
||||
<Section section={section} placement="sidebar" />
|
||||
</Fragment>
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.mainColumn}>
|
||||
{featuredSummarySection && (
|
||||
<View style={styles.specialContainer}>
|
||||
<SemanticRegionTemplatePartView
|
||||
region="featured"
|
||||
partKeys={["featured-summary"]}
|
||||
style={styles.specialContainer}
|
||||
>
|
||||
<Section section={featuredSummarySection} placement="main" showHeading={false} />
|
||||
</View>
|
||||
</SemanticRegionTemplatePartView>
|
||||
)}
|
||||
|
||||
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
{regularMainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</View>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -113,8 +133,8 @@ const Header = ({ styles, colors }: GengarHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -123,7 +143,7 @@ const Header = ({ styles, colors }: GengarHeaderProps) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem
|
||||
email={basics.email}
|
||||
style={styles.contactItem}
|
||||
@@ -157,8 +177,8 @@ const Header = ({ styles, colors }: GengarHeaderProps) => {
|
||||
iconColor={colors.background}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const gengarSemanticManifest = {
|
||||
template: "gengar",
|
||||
regions: [
|
||||
{ name: "header", placement: "sidebar", origins: [] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
{ name: "featured", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
],
|
||||
header: { region: "header", placement: "sidebar" },
|
||||
specialSummary: { region: "featured", placement: "main", source: "main-with-header" },
|
||||
parts: [
|
||||
{
|
||||
name: "featured-summary",
|
||||
key: "featured-summary",
|
||||
owner: { kind: "region", key: "featured" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "start", take: "all" },
|
||||
},
|
||||
{
|
||||
name: "sidebar-background",
|
||||
key: "sidebar-background",
|
||||
owner: { kind: "region", key: "sidebar" },
|
||||
binding: { type: "alias", canonicalKind: "region", token: "sidebar-background" },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import { getPrimaryTint } from "../shared/color-helpers";
|
||||
import {
|
||||
@@ -18,7 +20,15 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
SemanticTemplatePartView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
@@ -53,19 +63,31 @@ const glalieFeatures = {
|
||||
stackSidebarItemHeader: true,
|
||||
} satisfies TemplateFeatures;
|
||||
|
||||
export const GlaliePage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const GlaliePage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useGlalieTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const showSidebar = !page.fullWidth || showHeader;
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors} features={glalieFeatures}>
|
||||
{showSidebar && <View style={styles.sidebarBackground} />}
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors} features={glalieFeatures}>
|
||||
{showSidebar && (
|
||||
<SemanticTemplatePartView
|
||||
ownerNodeKey={semanticNodeKeys.region(pageNodeKey, "sidebar")}
|
||||
partKeys={["sidebar-background"]}
|
||||
style={styles.sidebarBackground}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.layout}>
|
||||
{showSidebar && (
|
||||
@@ -77,21 +99,24 @@ export const GlaliePage = ({ page, pageSize, pageMinHeightStyle, showHeader }: T
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}
|
||||
>
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.mainColumn}>
|
||||
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</View>
|
||||
</View>
|
||||
</TemplateProvider>
|
||||
@@ -104,8 +129,8 @@ const Header = ({ styles }: GlalieHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -114,7 +139,7 @@ const Header = ({ styles }: GlalieHeaderProps) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} />
|
||||
@@ -122,8 +147,8 @@ const Header = ({ styles }: GlalieHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const glalieSemanticManifest = {
|
||||
template: "glalie",
|
||||
regions: [
|
||||
{ name: "header", placement: "sidebar", origins: [] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
],
|
||||
header: { region: "header", placement: "sidebar" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "sidebar-background",
|
||||
key: "sidebar-background",
|
||||
owner: { kind: "region", key: "sidebar" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "start" },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -2,11 +2,17 @@ import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const registry = readFileSync(fileURLToPath(new URL("./index.ts", import.meta.url)), "utf8");
|
||||
|
||||
describe("templatePages", () => {
|
||||
it("registers Scizor as a renderable template page", () => {
|
||||
const registry = readFileSync(fileURLToPath(new URL("./index.ts", import.meta.url)), "utf8");
|
||||
|
||||
expect(registry).toContain('import { ScizorPage } from "./scizor/ScizorPage";');
|
||||
expect(registry).toContain("scizor: ScizorPage");
|
||||
});
|
||||
|
||||
it("exports the semantic manifest registry through the template index", () => {
|
||||
expect(registry).toContain("getTemplateSemanticManifest");
|
||||
expect(registry).toContain("getTemplateSemanticRegistryFingerprintInput");
|
||||
expect(registry).toContain("TemplateSemanticManifest");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,3 +37,11 @@ export const templatePages: Partial<Record<Template, TemplatePage>> = {
|
||||
const defaultTemplatePage = AzurillPage;
|
||||
|
||||
export const getTemplatePage = (template: Template): TemplatePage => templatePages[template] ?? defaultTemplatePage;
|
||||
|
||||
export type { TemplateSemanticManifest } from "../semantic/template-manifest";
|
||||
export {
|
||||
getTemplateSemanticBindingRegistry,
|
||||
getTemplateSemanticManifest,
|
||||
getTemplateSemanticRegistryFingerprintInput,
|
||||
validateTemplateSemanticManifest,
|
||||
} from "../semantic/template-manifest";
|
||||
|
||||
@@ -3,8 +3,10 @@ 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 { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,14 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -44,31 +53,40 @@ type KakunaHeaderProps = {
|
||||
styles: KakunaStyles;
|
||||
};
|
||||
|
||||
export const KakunaPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const KakunaPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useKakunaTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}
|
||||
>
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -80,8 +98,8 @@ const Header = ({ styles }: KakunaHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerCopy}>
|
||||
@@ -89,7 +107,7 @@ const Header = ({ styles }: KakunaHeaderProps) => {
|
||||
<Text style={styles.headerText}>{basics.headline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} />
|
||||
@@ -97,9 +115,9 @@ const Header = ({ styles }: KakunaHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
))}
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const kakunaSemanticManifest = {
|
||||
template: "kakuna",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ 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 { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,14 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -43,31 +52,40 @@ type LaprasHeaderProps = {
|
||||
styles: LaprasStyles;
|
||||
};
|
||||
|
||||
export const LaprasPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const LaprasPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useLaprasTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.gapY(1.5) })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sectionGroup, { rowGap: metrics.gapY(1.5) })}>
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.gapY(1.5) })}>
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sectionGroup, { rowGap: metrics.gapY(1.5) })}
|
||||
>
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -79,8 +97,8 @@ const Header = ({ styles }: LaprasHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -88,7 +106,7 @@ const Header = ({ styles }: LaprasHeaderProps) => {
|
||||
<Text>{basics.headline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} />
|
||||
@@ -96,9 +114,9 @@ const Header = ({ styles }: LaprasHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
))}
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const laprasSemanticManifest = {
|
||||
template: "lapras",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ 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 { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import { getPrimaryTint as getPrimaryAlpha } from "../shared/color-helpers";
|
||||
import {
|
||||
@@ -18,7 +20,15 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
SemanticTemplatePartView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -49,28 +59,41 @@ type LeafishHeaderProps = {
|
||||
styles: LeafishStyles;
|
||||
};
|
||||
|
||||
export const LeafishPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const LeafishPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useLeafishTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = filterSections(page.main, data).filter((section) => section !== "summary");
|
||||
const sidebarSections = filterSections(page.sidebar, data).filter((section) => section !== "summary");
|
||||
const mainSections = useRenderedSectionIds(
|
||||
pageNodeKey,
|
||||
filterSections(page.main, data).filter((section) => section !== "summary"),
|
||||
);
|
||||
const sidebarSections = useRenderedSectionIds(
|
||||
pageNodeKey,
|
||||
filterSections(page.sidebar, data).filter((section) => section !== "summary"),
|
||||
);
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={styles.body}>
|
||||
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sidebarColumn, {
|
||||
width: `${metadata.layout.sidebarWidth}%`,
|
||||
rowGap: metrics.sectionGap,
|
||||
@@ -79,7 +102,7 @@ export const LeafishPage = ({ page, pageSize, pageMinHeightStyle, showHeader }:
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</View>
|
||||
</TemplateProvider>
|
||||
@@ -92,10 +115,10 @@ const Header = ({ styles }: LeafishHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerIntro}>
|
||||
<View style={styles.headerBody}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
<SemanticTemplatePartView partKeys={["header-intro"]} style={styles.headerIntro}>
|
||||
<SemanticTemplatePartView partKeys={["header-intro", "header-body"]} style={styles.headerBody}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -105,11 +128,11 @@ const Header = ({ styles }: LeafishHeaderProps) => {
|
||||
|
||||
<Section section="summary" placement="main" showHeading={false} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticTemplatePartView>
|
||||
</SemanticTemplatePartView>
|
||||
|
||||
<View style={styles.headerContactBand}>
|
||||
<View style={styles.contactList}>
|
||||
<SemanticTemplatePartView partKeys={["header-contact-band"]} style={styles.headerContactBand}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} />
|
||||
@@ -117,9 +140,9 @@ const Header = ({ styles }: LeafishHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</SemanticTemplatePartView>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const leafishSemanticManifest = {
|
||||
template: "leafish",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: { region: "header", placement: "main", source: "always" },
|
||||
parts: [
|
||||
{
|
||||
name: "header-intro",
|
||||
key: "header-intro",
|
||||
owner: { kind: "header", key: "header" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: {
|
||||
parent: "owner",
|
||||
at: "start",
|
||||
take: [{ kind: "picture" }, { kind: "name" }, { kind: "headline" }, { kind: "section", name: "summary" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "header-body",
|
||||
key: "header-body",
|
||||
owner: { kind: "header", key: "header" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "header-intro", at: "start", take: "all" },
|
||||
},
|
||||
{
|
||||
name: "header-contact-band",
|
||||
key: "header-contact-band",
|
||||
owner: { kind: "header", key: "header" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "end", take: [{ kind: "contact-list" }] },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,14 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -48,31 +57,40 @@ const meowthFeatures = {
|
||||
inlineItemHeader: true,
|
||||
} satisfies TemplateFeatures;
|
||||
|
||||
export const MeowthPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const MeowthPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useMeowthTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors} features={meowthFeatures}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors} features={meowthFeatures}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}
|
||||
>
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -84,14 +102,14 @@ const Header = ({ styles }: MeowthHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
<Heading style={styles.headerName}>{basics.name}</Heading>
|
||||
<Text style={styles.headerHeadline}>{basics.headline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} />
|
||||
@@ -99,11 +117,11 @@ const Header = ({ styles }: MeowthHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
))}
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
</View>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
const inlineHeaderSectionTypes = ["experience", "education", "volunteer"] as const;
|
||||
|
||||
export const meowthSemanticManifest = {
|
||||
template: "meowth",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "inline-item-header-leading",
|
||||
key: "inline-item-header-leading",
|
||||
owner: { kind: "item-header", key: "item-header", sectionTypes: inlineHeaderSectionTypes },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: {
|
||||
parent: "owner",
|
||||
at: "start",
|
||||
take: [
|
||||
{ kind: "field", name: "position", sectionTypes: ["experience", "volunteer"] },
|
||||
{ kind: "field", name: "location", sectionTypes: ["experience", "volunteer"] },
|
||||
{ kind: "field", name: "area", sectionTypes: ["education"] },
|
||||
{ kind: "field", name: "degree", sectionTypes: ["education"] },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inline-item-header-middle",
|
||||
key: "inline-item-header-middle",
|
||||
owner: { kind: "item-header", key: "item-header", sectionTypes: inlineHeaderSectionTypes },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: {
|
||||
parent: "owner",
|
||||
at: "start",
|
||||
take: [
|
||||
{ kind: "field", name: "company" },
|
||||
{ kind: "field", name: "school" },
|
||||
{ kind: "field", name: "organization" },
|
||||
{ kind: "link" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inline-item-header-trailing",
|
||||
key: "inline-item-header-trailing",
|
||||
owner: { kind: "item-header", key: "item-header", sectionTypes: inlineHeaderSectionTypes },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "start", take: [{ kind: "field", name: "period" }] },
|
||||
},
|
||||
{
|
||||
name: "education-grade-row",
|
||||
key: "education-grade-row",
|
||||
owner: { kind: "item", key: "item" },
|
||||
binding: { type: "primitive", primitive: "Text", source: "existing" },
|
||||
route: {
|
||||
parent: "owner",
|
||||
at: { after: { kind: "item-header" } },
|
||||
take: [
|
||||
{ kind: "field", name: "grade", sectionTypes: ["education"] },
|
||||
{ kind: "field", name: "location", sectionTypes: ["education"] },
|
||||
],
|
||||
takeFrom: "item-header",
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ 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 { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,14 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -43,31 +52,40 @@ type OnyxHeaderProps = {
|
||||
styles: OnyxStyles;
|
||||
};
|
||||
|
||||
export const OnyxPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const OnyxPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useOnyxTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}
|
||||
>
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -79,8 +97,8 @@ const Header = ({ styles }: OnyxHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
@@ -88,7 +106,7 @@ const Header = ({ styles }: OnyxHeaderProps) => {
|
||||
<Text>{basics.headline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} />
|
||||
@@ -96,9 +114,9 @@ const Header = ({ styles }: OnyxHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
))}
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const onyxSemanticManifest = {
|
||||
template: "onyx",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ import type { TemplatePageProps } from "../../document";
|
||||
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { useMemo } from "react";
|
||||
import { rgbaStringToHex } from "@reactive-resume/utils/color";
|
||||
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,15 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
SemanticTemplatePartView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight, resolvePlacementColor } from "../shared/styles";
|
||||
@@ -53,19 +63,25 @@ const pikachuFeatures = {
|
||||
stackSidebarItemHeader: true,
|
||||
} satisfies TemplateFeatures;
|
||||
|
||||
export const PikachuPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const PikachuPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata, picture } = data;
|
||||
const { colors, styles } = usePikachuTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const showSidebar = !page.fullWidth;
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors} features={pikachuFeatures}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors} features={pikachuFeatures}>
|
||||
<View style={styles.layout}>
|
||||
{showSidebar && (
|
||||
<View
|
||||
@@ -74,20 +90,27 @@ export const PikachuPage = ({ page, pageSize, pageMinHeightStyle, showHeader }:
|
||||
rowGap: metrics.sectionGap,
|
||||
})}
|
||||
>
|
||||
{showHeader && showSidebar && hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
{showHeader && showSidebar && hasPicture && (
|
||||
<SemanticHeaderPicture src={picture.url} style={styles.picture} />
|
||||
)}
|
||||
|
||||
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}
|
||||
>
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
{showHeader && (
|
||||
<View style={styles.headerRow}>
|
||||
{showHeader && !showSidebar && hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
{showHeader && !showSidebar && hasPicture && (
|
||||
<SemanticHeaderPicture src={picture.url} style={styles.picture} />
|
||||
)}
|
||||
<Header styles={styles} colors={colors} />
|
||||
</View>
|
||||
)}
|
||||
@@ -97,7 +120,7 @@ export const PikachuPage = ({ page, pageSize, pageMinHeightStyle, showHeader }:
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</View>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -108,15 +131,15 @@ const Header = ({ styles, colors }: PikachuHeaderProps) => {
|
||||
const { basics } = useRender();
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerDivider}>
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
<SemanticTemplatePartView partKeys={["header-divider"]} style={styles.headerDivider}>
|
||||
<View style={styles.headerIdentity}>
|
||||
<Heading style={styles.headerName}>{basics.name}</Heading>
|
||||
<Text style={styles.headerText}>{basics.headline}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</SemanticTemplatePartView>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem
|
||||
email={basics.email}
|
||||
style={styles.contactItem}
|
||||
@@ -150,8 +173,8 @@ const Header = ({ styles, colors }: PikachuHeaderProps) => {
|
||||
iconColor={colors.background}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const pikachuSemanticManifest = {
|
||||
template: "pikachu",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "header-divider",
|
||||
key: "header-divider",
|
||||
owner: { kind: "header", key: "header" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: "start", take: [{ kind: "name" }, { kind: "headline" }] },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -4,8 +4,11 @@ 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 { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { resolvedPdfFlowProps } from "../../semantic/adapter";
|
||||
import { useRenderedSectionIds, useResolvedNode, useSemanticNodeVisible } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -14,11 +17,19 @@ import {
|
||||
PhoneContactItem,
|
||||
WebsiteContactItem,
|
||||
} from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { TemplateProvider, useTemplatePageNodeKey } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
semanticTemplatePartNodeKey,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -46,31 +57,62 @@ type RhyhornHeaderProps = {
|
||||
styles: RhyhornStyles;
|
||||
};
|
||||
|
||||
export const RhyhornPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
type RhyhornContactItemProps = {
|
||||
nodeKey: string | undefined;
|
||||
last: boolean;
|
||||
styles: RhyhornStyles;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const RhyhornContactItem = ({ nodeKey, last, styles, children }: RhyhornContactItemProps) => {
|
||||
const resolved = useResolvedNode(nodeKey);
|
||||
const visible = useSemanticNodeVisible(nodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...resolvedPdfFlowProps(resolved)}
|
||||
style={composeStyles(styles.contactItem, last ? styles.contactItemLast : undefined, resolved.style)}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const RhyhornPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useRhyhornTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, filterSections(page.sidebar, data));
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
{mainSections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
|
||||
{!page.fullWidth && (
|
||||
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView
|
||||
region="sidebar"
|
||||
style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}
|
||||
>
|
||||
{sidebarSections.map((section) => (
|
||||
<Section key={section} section={section} placement="sidebar" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
)}
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
@@ -80,69 +122,111 @@ export const RhyhornPage = ({ page, pageSize, pageMinHeightStyle, showHeader }:
|
||||
const Header = ({ styles }: RhyhornHeaderProps) => {
|
||||
const { basics, picture } = useRender();
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
const pageNodeKey = useTemplatePageNodeKey();
|
||||
const headerNodeKey = semanticNodeKeys.header(semanticNodeKeys.region(pageNodeKey, "header"));
|
||||
const contactListNodeKey = headerNodeKey ? semanticNodeKeys.contactList(headerNodeKey) : undefined;
|
||||
const contactNodeKey = (name: string, id?: string) =>
|
||||
contactListNodeKey ? semanticNodeKeys.contactItem(contactListNodeKey, name, id) : undefined;
|
||||
const contentNodeKey = (name: string, id?: string) =>
|
||||
semanticTemplatePartNodeKey(contactNodeKey(name, id), "contact-item-content");
|
||||
const contactItems: {
|
||||
id: string;
|
||||
nodeKey: string | undefined;
|
||||
content: ReactNode;
|
||||
}[] = [];
|
||||
|
||||
if (basics.email) {
|
||||
contactItems.push({
|
||||
id: "email",
|
||||
content: <EmailContactItem email={basics.email} style={styles.contactItemContent} />,
|
||||
nodeKey: contactNodeKey("email"),
|
||||
content: (
|
||||
<EmailContactItem
|
||||
email={basics.email}
|
||||
primitiveNodeKey={contentNodeKey("email")}
|
||||
style={styles.contactItemContent}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (basics.phone) {
|
||||
contactItems.push({
|
||||
id: "phone",
|
||||
content: <PhoneContactItem phone={basics.phone} style={styles.contactItemContent} />,
|
||||
nodeKey: contactNodeKey("phone"),
|
||||
content: (
|
||||
<PhoneContactItem
|
||||
phone={basics.phone}
|
||||
primitiveNodeKey={contentNodeKey("phone")}
|
||||
style={styles.contactItemContent}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (basics.location) {
|
||||
contactItems.push({
|
||||
id: "location",
|
||||
content: <LocationContactItem location={basics.location} style={styles.contactItemContent} />,
|
||||
nodeKey: contactNodeKey("location"),
|
||||
content: (
|
||||
<LocationContactItem
|
||||
location={basics.location}
|
||||
primitiveNodeKey={contentNodeKey("location")}
|
||||
style={styles.contactItemContent}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (basics.website.url) {
|
||||
contactItems.push({
|
||||
id: "website",
|
||||
content: <WebsiteContactItem website={basics.website} style={styles.contactItemContent} />,
|
||||
nodeKey: contactNodeKey("website"),
|
||||
content: (
|
||||
<WebsiteContactItem
|
||||
website={basics.website}
|
||||
primitiveNodeKey={contentNodeKey("website")}
|
||||
style={styles.contactItemContent}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
contactItems.push(
|
||||
...basics.customFields.map((field) => ({
|
||||
id: `custom-${field.id}`,
|
||||
content: <CustomFieldContactItem field={field} style={styles.contactItemContent} />,
|
||||
nodeKey: contactNodeKey("custom", field.id),
|
||||
content: (
|
||||
<CustomFieldContactItem
|
||||
field={field}
|
||||
primitiveNodeKey={contentNodeKey("custom", field.id)}
|
||||
style={styles.contactItemContent}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
<View style={styles.headerTitle}>
|
||||
<View style={styles.headerIdentity}>
|
||||
<Heading style={styles.headerName}>{basics.name}</Heading>
|
||||
<Text>{basics.headline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.contactList}>
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
{contactItems.map((item, index) => (
|
||||
<View
|
||||
<RhyhornContactItem
|
||||
key={item.id}
|
||||
style={composeStyles(
|
||||
styles.contactItem,
|
||||
index === contactItems.length - 1 ? styles.contactItemLast : undefined,
|
||||
)}
|
||||
nodeKey={item.nodeKey}
|
||||
last={index === contactItems.length - 1}
|
||||
styles={styles}
|
||||
>
|
||||
{item.content}
|
||||
</View>
|
||||
</RhyhornContactItem>
|
||||
))}
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
</View>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const rhyhornSemanticManifest = {
|
||||
template: "rhyhorn",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main"] },
|
||||
{ name: "sidebar", placement: "sidebar", origins: ["sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "contact-item-content",
|
||||
key: "contact-item-content",
|
||||
owner: { kind: "contact-item", key: "contact-item" },
|
||||
binding: {
|
||||
type: "primitive",
|
||||
primitive: { ownerRole: "structured-link", present: "Link", absent: "View" },
|
||||
source: "existing",
|
||||
},
|
||||
route: { parent: "owner", at: "start", take: "all" },
|
||||
},
|
||||
{
|
||||
name: "contact-item-last",
|
||||
key: "contact-item-last",
|
||||
owner: { kind: "contact-item", key: "contact-item", position: "last" },
|
||||
binding: { type: "alias", canonicalKind: "contact-item", token: "contact-item-last" },
|
||||
},
|
||||
],
|
||||
canonicalBindings: [{ kind: "contact-item", binding: { type: "primitive", primitive: "View", source: "existing" } }],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -3,8 +3,10 @@ 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 { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { Page, StyleSheet, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useRenderedSectionIds, useResolvedNode } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { createBaseTemplateStyles } from "../shared/base-template-styles";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
@@ -17,7 +19,15 @@ import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import { Heading, Text } from "../shared/primitives";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
SemanticTemplatePartView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { createRtlStyleHelpers } from "../shared/rtl";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
@@ -44,25 +54,31 @@ type ScizorHeaderProps = {
|
||||
styles: ScizorStyles;
|
||||
};
|
||||
|
||||
export const ScizorPage = ({ page, pageSize, pageMinHeightStyle, showHeader }: TemplatePageProps) => {
|
||||
export const ScizorPage = ({ page, pageSize, pageMinHeightStyle, showHeader, pageNumber }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const pageNodeKey = semanticNodeKeys.page(pageNumber);
|
||||
const { style: semanticPageStyle, size: semanticPageSize, ...semanticPageProps } = useResolvedNode(pageNodeKey);
|
||||
const { metadata } = data;
|
||||
const { colors, styles } = useScizorTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = page.fullWidth ? [] : filterSections(page.sidebar, data);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, page.fullWidth ? [] : filterSections(page.sidebar, data));
|
||||
const sections = [...mainSections, ...sidebarSections];
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors}>
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<View style={composeStyles(styles.sections, { rowGap: metrics.sectionGap })}>
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sections, { rowGap: metrics.sectionGap })}>
|
||||
{sections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</View>
|
||||
</SemanticRegionView>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
);
|
||||
@@ -73,13 +89,13 @@ const Header = ({ styles }: ScizorHeaderProps) => {
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
<View style={styles.headerIdentity}>
|
||||
<Heading style={styles.headerName}>{basics.name}</Heading>
|
||||
<View style={styles.headerNameRule} />
|
||||
<SemanticTemplatePartView partKeys={["header-name-rule"]} style={styles.headerNameRule} />
|
||||
{basics.headline && <Text style={styles.headerHeadline}>{basics.headline}</Text>}
|
||||
|
||||
<View style={styles.headerContactRow}>
|
||||
<SemanticContactListView style={styles.headerContactRow}>
|
||||
<LocationContactItem location={basics.location} style={styles.headerContactItem} />
|
||||
<EmailContactItem email={basics.email} style={styles.headerContactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.headerContactItem} />
|
||||
@@ -87,11 +103,11 @@ const Header = ({ styles }: ScizorHeaderProps) => {
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.headerContactItem} />
|
||||
))}
|
||||
</View>
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
|
||||
{hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
</View>
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
|
||||
export const scizorSemanticManifest = {
|
||||
template: "scizor",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main", "sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [
|
||||
{
|
||||
name: "header-name-rule",
|
||||
key: "header-name-rule",
|
||||
owner: { kind: "header", key: "header" },
|
||||
binding: { type: "primitive", primitive: "View", source: "existing" },
|
||||
route: { parent: "owner", at: { before: { kind: "headline" } } },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -2,8 +2,12 @@ import type { Style } from "@react-pdf/types";
|
||||
import type { CustomField } from "@reactive-resume/schema/resume/data";
|
||||
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
|
||||
import { View } from "#react-pdf-renderer";
|
||||
import { resolvedPdfFlowProps } from "../../semantic/adapter";
|
||||
import { useResolvedNode, useSemanticNodeKey, useSemanticNodeVisible } from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { getCustomFieldLinkUrl, getWebsiteDisplayText } from "./contact";
|
||||
import { Icon, Link, Text } from "./primitives";
|
||||
import { composeStyles } from "./styles";
|
||||
|
||||
type ContactStyle = Style | Style[];
|
||||
|
||||
@@ -12,11 +16,25 @@ type WebsiteDisplay = {
|
||||
label?: string | undefined;
|
||||
};
|
||||
|
||||
const useContactNodeKeys = (name: string, id?: string, primitiveNodeKey?: string) => {
|
||||
const headerNodeKey = useSemanticNodeKey();
|
||||
const contactListNodeKey = headerNodeKey ? semanticNodeKeys.contactList(headerNodeKey) : undefined;
|
||||
const contactNodeKey = contactListNodeKey ? semanticNodeKeys.contactItem(contactListNodeKey, name, id) : undefined;
|
||||
|
||||
return {
|
||||
contactNodeKey,
|
||||
primitiveNodeKey: primitiveNodeKey ?? contactNodeKey,
|
||||
fieldNodeKey: contactNodeKey ? semanticNodeKeys.field(contactNodeKey, name) : undefined,
|
||||
iconNodeKey: contactNodeKey ? semanticNodeKeys.icon(contactNodeKey, "contact") : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
type WebsiteContactItemProps = {
|
||||
website: WebsiteDisplay;
|
||||
style?: ContactStyle;
|
||||
textStyle?: ContactStyle;
|
||||
iconColor?: string;
|
||||
primitiveNodeKey?: string | undefined;
|
||||
};
|
||||
|
||||
type CustomFieldContactItemProps = {
|
||||
@@ -24,37 +42,64 @@ type CustomFieldContactItemProps = {
|
||||
style?: ContactStyle;
|
||||
textStyle?: ContactStyle;
|
||||
iconColor?: string;
|
||||
primitiveNodeKey?: string | undefined;
|
||||
};
|
||||
|
||||
export const WebsiteContactItem = ({ website, style, textStyle, iconColor }: WebsiteContactItemProps) => {
|
||||
if (!website.url) return null;
|
||||
export const WebsiteContactItem = ({
|
||||
website,
|
||||
style,
|
||||
textStyle,
|
||||
iconColor,
|
||||
primitiveNodeKey,
|
||||
}: WebsiteContactItemProps) => {
|
||||
const keys = useContactNodeKeys("website", undefined, primitiveNodeKey);
|
||||
const visible = useSemanticNodeVisible(keys.primitiveNodeKey);
|
||||
if (!website.url || !visible) return null;
|
||||
|
||||
return (
|
||||
<Link src={website.url} {...(style ? { style } : {})}>
|
||||
<Icon name="globe" {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text {...(textStyle ? { style: textStyle } : {})}>{getWebsiteDisplayText(website)}</Text>
|
||||
<Link nodeKey={keys.primitiveNodeKey} src={website.url} {...(style ? { style } : {})}>
|
||||
<Icon nodeKey={keys.iconNodeKey} name="globe" {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text nodeKey={keys.fieldNodeKey} {...(textStyle ? { style: textStyle } : {})}>
|
||||
{getWebsiteDisplayText(website)}
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomFieldContactItem = ({ field, style, textStyle, iconColor }: CustomFieldContactItemProps) => {
|
||||
export const CustomFieldContactItem = ({
|
||||
field,
|
||||
style,
|
||||
textStyle,
|
||||
iconColor,
|
||||
primitiveNodeKey,
|
||||
}: CustomFieldContactItemProps) => {
|
||||
const linkUrl = getCustomFieldLinkUrl(field);
|
||||
const keys = useContactNodeKeys("custom", field.id, primitiveNodeKey);
|
||||
const resolved = useResolvedNode(keys.primitiveNodeKey);
|
||||
const visible = useSemanticNodeVisible(keys.primitiveNodeKey);
|
||||
const children = (
|
||||
<>
|
||||
<Icon name={field.icon as IconName} {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text {...(textStyle ? { style: textStyle } : {})}>{field.text}</Text>
|
||||
<Icon nodeKey={keys.iconNodeKey} name={field.icon as IconName} {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text nodeKey={keys.fieldNodeKey} {...(textStyle ? { style: textStyle } : {})}>
|
||||
{field.text}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
if (!visible) return null;
|
||||
|
||||
if (linkUrl) {
|
||||
return (
|
||||
<Link src={linkUrl} {...(style ? { style } : {})}>
|
||||
<Link nodeKey={keys.primitiveNodeKey} src={linkUrl} {...(style ? { style } : {})}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <View {...(style ? { style } : {})}>{children}</View>;
|
||||
return (
|
||||
<View {...resolvedPdfFlowProps(resolved)} style={composeStyles(style, resolved.style)}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type EmailContactItemProps = {
|
||||
@@ -64,6 +109,7 @@ type EmailContactItemProps = {
|
||||
iconColor?: string;
|
||||
/** Override icon; defaults to "envelope". ditgar uses "at". */
|
||||
iconName?: IconName;
|
||||
primitiveNodeKey?: string | undefined;
|
||||
};
|
||||
|
||||
export const EmailContactItem = ({
|
||||
@@ -72,12 +118,17 @@ export const EmailContactItem = ({
|
||||
textStyle,
|
||||
iconColor,
|
||||
iconName = "envelope",
|
||||
primitiveNodeKey,
|
||||
}: EmailContactItemProps) => {
|
||||
if (!email) return null;
|
||||
const keys = useContactNodeKeys("email", undefined, primitiveNodeKey);
|
||||
const visible = useSemanticNodeVisible(keys.primitiveNodeKey);
|
||||
if (!email || !visible) return null;
|
||||
return (
|
||||
<Link src={`mailto:${email}`} {...(style ? { style } : {})}>
|
||||
<Icon name={iconName} {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text {...(textStyle ? { style: textStyle } : {})}>{email}</Text>
|
||||
<Link nodeKey={keys.primitiveNodeKey} src={`mailto:${email}`} {...(style ? { style } : {})}>
|
||||
<Icon nodeKey={keys.iconNodeKey} name={iconName} {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text nodeKey={keys.fieldNodeKey} {...(textStyle ? { style: textStyle } : {})}>
|
||||
{email}
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -87,14 +138,19 @@ type PhoneContactItemProps = {
|
||||
style?: ContactStyle;
|
||||
textStyle?: ContactStyle;
|
||||
iconColor?: string;
|
||||
primitiveNodeKey?: string | undefined;
|
||||
};
|
||||
|
||||
export const PhoneContactItem = ({ phone, style, textStyle, iconColor }: PhoneContactItemProps) => {
|
||||
if (!phone) return null;
|
||||
export const PhoneContactItem = ({ phone, style, textStyle, iconColor, primitiveNodeKey }: PhoneContactItemProps) => {
|
||||
const keys = useContactNodeKeys("phone", undefined, primitiveNodeKey);
|
||||
const visible = useSemanticNodeVisible(keys.primitiveNodeKey);
|
||||
if (!phone || !visible) return null;
|
||||
return (
|
||||
<Link src={`tel:${phone}`} {...(style ? { style } : {})}>
|
||||
<Icon name="phone" {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text {...(textStyle ? { style: textStyle } : {})}>{phone}</Text>
|
||||
<Link nodeKey={keys.primitiveNodeKey} src={`tel:${phone}`} {...(style ? { style } : {})}>
|
||||
<Icon nodeKey={keys.iconNodeKey} name="phone" {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text nodeKey={keys.fieldNodeKey} {...(textStyle ? { style: textStyle } : {})}>
|
||||
{phone}
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -104,14 +160,26 @@ type LocationContactItemProps = {
|
||||
style?: ContactStyle;
|
||||
textStyle?: ContactStyle;
|
||||
iconColor?: string;
|
||||
primitiveNodeKey?: string | undefined;
|
||||
};
|
||||
|
||||
export const LocationContactItem = ({ location, style, textStyle, iconColor }: LocationContactItemProps) => {
|
||||
if (!location) return null;
|
||||
export const LocationContactItem = ({
|
||||
location,
|
||||
style,
|
||||
textStyle,
|
||||
iconColor,
|
||||
primitiveNodeKey,
|
||||
}: LocationContactItemProps) => {
|
||||
const keys = useContactNodeKeys("location", undefined, primitiveNodeKey);
|
||||
const resolved = useResolvedNode(keys.primitiveNodeKey);
|
||||
const visible = useSemanticNodeVisible(keys.primitiveNodeKey);
|
||||
if (!location || !visible) return null;
|
||||
return (
|
||||
<View {...(style ? { style } : {})}>
|
||||
<Icon name="map-pin" {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text {...(textStyle ? { style: textStyle } : {})}>{location}</Text>
|
||||
<View {...resolvedPdfFlowProps(resolved)} style={composeStyles(style, resolved.style)}>
|
||||
<Icon nodeKey={keys.iconNodeKey} name="map-pin" {...(iconColor ? { color: iconColor } : {})} />
|
||||
<Text nodeKey={keys.fieldNodeKey} {...(textStyle ? { style: textStyle } : {})}>
|
||||
{location}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
} from "./types";
|
||||
import { createContext, use, useMemo } from "react";
|
||||
import { useRender } from "../../context";
|
||||
import { useSemanticRenderMode } from "../../semantic/context";
|
||||
import { resolveStyleRuleSlot } from "./style-rules";
|
||||
|
||||
type TemplateContextValue = {
|
||||
@@ -21,11 +22,13 @@ type TemplateContextValue = {
|
||||
featureStyles: TemplateFeatureStyleSlots;
|
||||
colors: TemplateColorRoles;
|
||||
features: TemplateFeatures;
|
||||
pageNodeKey: string;
|
||||
};
|
||||
|
||||
type TemplateProviderProps = Omit<TemplateContextValue, "featureStyles" | "features" | "sectionTitleFallbacks"> & {
|
||||
featureStyles?: TemplateFeatureStyleSlots;
|
||||
features?: TemplateFeatures;
|
||||
pageNodeKey: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -73,11 +76,12 @@ export const TemplateProvider = ({
|
||||
featureStyles = EMPTY_FEATURE_STYLES,
|
||||
colors,
|
||||
features = EMPTY_FEATURES,
|
||||
pageNodeKey,
|
||||
children,
|
||||
}: TemplateProviderProps) => {
|
||||
const contextValue = useMemo<TemplateContextValue>(
|
||||
() => ({ styles, featureStyles, colors, features }),
|
||||
[colors, featureStyles, features, styles],
|
||||
() => ({ styles, featureStyles, colors, features, pageNodeKey }),
|
||||
[colors, featureStyles, features, pageNodeKey, styles],
|
||||
);
|
||||
|
||||
return <TemplateContext.Provider value={contextValue}>{children}</TemplateContext.Provider>;
|
||||
@@ -107,6 +111,8 @@ export const useTemplateFeature = (feature: keyof TemplateFeatures): boolean =>
|
||||
|
||||
export const useTemplatePlacement = () => use(TemplatePlacementContext);
|
||||
|
||||
export const useTemplatePageNodeKey = () => useTemplateContext().pageNodeKey;
|
||||
|
||||
const useTemplateStyleContext = (): TemplateStyleContextValue => {
|
||||
const { colors } = useTemplateContext();
|
||||
const placement = useTemplatePlacement();
|
||||
@@ -124,8 +130,9 @@ export const useTemplateStyle = (slot: keyof TemplateStyleSlots): StyleInput =>
|
||||
export const useSectionStyleRule = (slot: StyleSlot): StyleInput => {
|
||||
const data = useRender();
|
||||
const context = use(SectionStyleContext);
|
||||
const mode = useSemanticRenderMode();
|
||||
|
||||
if (!context) return undefined;
|
||||
if (!context || mode === "semantic") return undefined;
|
||||
|
||||
return resolveStyleRuleSlot(data, { ...context, slot });
|
||||
};
|
||||
|
||||
@@ -3,6 +3,14 @@ import type { IconName } from "phosphor-icons-react-pdf/dynamic";
|
||||
import { resolveLevelDisplaySizes } from "@reactive-resume/schema/resume/level-display-sizes";
|
||||
import { View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { resolvedPdfFlowProps } from "../../semantic/adapter";
|
||||
import {
|
||||
useResolvedNode,
|
||||
useSemanticNodeBindings,
|
||||
useSemanticNodeKey,
|
||||
useSemanticNodeVisible,
|
||||
} from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { useSectionStyleRule, useTemplateIconSlot, useTemplateStyle } from "./context";
|
||||
import { resolveStyleFontSize } from "./icon-size";
|
||||
import { getTemplateMetrics } from "./metrics";
|
||||
@@ -17,6 +25,11 @@ type LevelDisplayProps = {
|
||||
|
||||
export const LevelDisplay = ({ level }: LevelDisplayProps) => {
|
||||
const data = useRender();
|
||||
const itemNodeKey = useSemanticNodeKey();
|
||||
const levelNodeKey = itemNodeKey ? semanticNodeKeys.level(itemNodeKey) : undefined;
|
||||
const resolved = useResolvedNode(levelNodeKey);
|
||||
const visible = useSemanticNodeVisible(levelNodeKey);
|
||||
const { resolveNode, isNodeVisible } = useSemanticNodeBindings();
|
||||
const levelDesign = data.metadata.design.level;
|
||||
const metrics = getTemplateMetrics(data.metadata.page);
|
||||
const iconProps = useTemplateIconSlot("icon");
|
||||
@@ -29,11 +42,11 @@ export const LevelDisplay = ({ level }: LevelDisplayProps) => {
|
||||
const { decorationSize, levelIconExplicitSize } = resolveLevelDisplaySizes({
|
||||
bodyFontSize: data.metadata.typography.body.fontSize,
|
||||
iconFontSize: resolveStyleFontSize(iconRuleStyle),
|
||||
levelFontSize: resolveStyleFontSize(levelRuleStyle),
|
||||
levelFontSize: resolveStyleFontSize(levelRuleStyle, resolved.style),
|
||||
});
|
||||
const color = typeof iconProps.color === "string" ? iconProps.color : "#000000";
|
||||
|
||||
if (level === 0) return null;
|
||||
if (level === 0 || !visible) return null;
|
||||
if (levelDesign.type === "hidden") return null;
|
||||
if (levelDesign.type === "icon" && levelDesign.icon === "") return null;
|
||||
|
||||
@@ -49,19 +62,25 @@ export const LevelDisplay = ({ level }: LevelDisplayProps) => {
|
||||
|
||||
return (
|
||||
<View
|
||||
{...resolvedPdfFlowProps(resolved)}
|
||||
style={composeStyles(
|
||||
{ flexDirection: "row", alignItems: "center", marginTop: 2, columnGap: gap },
|
||||
levelContainerStyle,
|
||||
levelRuleStyle,
|
||||
resolved.style,
|
||||
)}
|
||||
>
|
||||
{LEVEL_ITEM_KEYS.map((itemKey, index) => {
|
||||
const isActive = index < level;
|
||||
const decorationNodeKey = levelNodeKey ? semanticNodeKeys.icon(levelNodeKey, itemKey) : undefined;
|
||||
const decorationResolved = resolveNode(decorationNodeKey);
|
||||
if (!isNodeVisible(decorationNodeKey)) return null;
|
||||
|
||||
if (levelDesign.type === "icon") {
|
||||
return (
|
||||
<Icon
|
||||
key={itemKey}
|
||||
nodeKey={decorationNodeKey}
|
||||
{...(levelIconExplicitSize === undefined ? {} : { size: levelIconExplicitSize })}
|
||||
name={levelDesign.icon as IconName}
|
||||
style={{ opacity: isActive ? 1 : 0.35 }}
|
||||
@@ -73,6 +92,7 @@ export const LevelDisplay = ({ level }: LevelDisplayProps) => {
|
||||
return (
|
||||
<View
|
||||
key={itemKey}
|
||||
{...resolvedPdfFlowProps(decorationResolved)}
|
||||
style={composeStyles(
|
||||
{
|
||||
flex: 1,
|
||||
@@ -83,6 +103,7 @@ export const LevelDisplay = ({ level }: LevelDisplayProps) => {
|
||||
},
|
||||
levelItemStyle,
|
||||
isActive ? levelItemActiveStyle : levelItemInactiveStyle,
|
||||
decorationResolved.style,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -108,6 +129,7 @@ export const LevelDisplay = ({ level }: LevelDisplayProps) => {
|
||||
return (
|
||||
<View
|
||||
key={itemKey}
|
||||
{...resolvedPdfFlowProps(decorationResolved)}
|
||||
style={composeStyles(
|
||||
{
|
||||
width,
|
||||
@@ -120,6 +142,7 @@ export const LevelDisplay = ({ level }: LevelDisplayProps) => {
|
||||
},
|
||||
levelItemStyle,
|
||||
isActive ? levelItemActiveStyle : levelItemInactiveStyle,
|
||||
decorationResolved.style,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,8 @@ const source = readFileSync(fileURLToPath(new URL("./primitives.tsx", import.met
|
||||
describe("Link", () => {
|
||||
it("passes the resume page underline preference to shared link styles", () => {
|
||||
expect(source).toContain("metadata.page.hideLinkUnderline");
|
||||
expect(source).toContain("hideUnderline: metadata.page.hideLinkUnderline");
|
||||
expect(source).toContain("{ hideUnderline: metadata.page.hideLinkUnderline }");
|
||||
expect(source).toContain("resolved.style");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,108 +1,465 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import type { StyleInput } from "./styles";
|
||||
import { Icon as PhosphorIcon } from "phosphor-icons-react-pdf/dynamic";
|
||||
import { Link as PdfLink, Text as PdfText, View } from "#react-pdf-renderer";
|
||||
import { Children, isValidElement } from "react";
|
||||
import { Image, Link as PdfLink, Text as PdfText, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { useSectionStyleRule, useTemplateIconSlot, useTemplateStyle } from "./context";
|
||||
import { resolvedPdfFlowProps, resolvedPdfTextProps } from "../../semantic/adapter";
|
||||
import {
|
||||
projectRenderedChildren,
|
||||
SemanticNodeKeyProvider,
|
||||
useRenderedChildKeys,
|
||||
useResolvedNode,
|
||||
useSemanticNodeExists,
|
||||
useSemanticNodeKey,
|
||||
useSemanticNodeVisible,
|
||||
} from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { useSectionStyleRule, useTemplateIconSlot, useTemplatePageNodeKey, useTemplateStyle } from "./context";
|
||||
import { resolveIconSize } from "./icon-size";
|
||||
import { safeTextStyle } from "./safe-text-style";
|
||||
import { composeLinkStyles, composeStyles } from "./styles";
|
||||
|
||||
const asStyleInput = (style: unknown): StyleInput => style as StyleInput;
|
||||
|
||||
export const Div = ({ style, ...props }: ComponentProps<typeof View>) => {
|
||||
type SemanticProps = {
|
||||
nodeKey?: string | undefined;
|
||||
semanticField?: string | undefined;
|
||||
bindSemanticNode?: boolean | undefined;
|
||||
bindCurrentNode?: boolean | undefined;
|
||||
};
|
||||
|
||||
type SemanticLinkProps = SemanticProps & {
|
||||
semanticRole?: string | undefined;
|
||||
};
|
||||
|
||||
const getChildren = (props: object): ReactNode =>
|
||||
"children" in props ? (props as { children?: ReactNode }).children : undefined;
|
||||
|
||||
const contactChildNodeKey = (contactListNodeKey: string, child: ReactNode): string | undefined => {
|
||||
if (!isValidElement(child)) return;
|
||||
const props = child.props as Record<string, unknown>;
|
||||
if (typeof props.primitiveNodeKey === "string") return props.primitiveNodeKey;
|
||||
if (typeof props.nodeKey === "string") return props.nodeKey;
|
||||
if (typeof props.partKey === "string") return semanticTemplatePartNodeKey(contactListNodeKey, props.partKey);
|
||||
if ("email" in props) return semanticNodeKeys.contactItem(contactListNodeKey, "email");
|
||||
if ("phone" in props) return semanticNodeKeys.contactItem(contactListNodeKey, "phone");
|
||||
if ("location" in props) return semanticNodeKeys.contactItem(contactListNodeKey, "location");
|
||||
if ("website" in props) return semanticNodeKeys.contactItem(contactListNodeKey, "website");
|
||||
if (typeof props.field === "object" && props.field !== null && "id" in props.field) {
|
||||
return semanticNodeKeys.contactItem(contactListNodeKey, "custom", String(props.field.id));
|
||||
}
|
||||
};
|
||||
|
||||
const projectContactChildren = (
|
||||
children: ReactNode,
|
||||
contactListNodeKey: string,
|
||||
renderedChildKeys: readonly string[],
|
||||
): ReactNode => {
|
||||
const authored = Children.toArray(children);
|
||||
const entries = authored.flatMap((child) => {
|
||||
const nodeKey = contactChildNodeKey(contactListNodeKey, child);
|
||||
return nodeKey ? [{ nodeKey, value: child }] : [];
|
||||
});
|
||||
|
||||
return entries.length === authored.length ? projectRenderedChildren(renderedChildKeys, entries) : authored;
|
||||
};
|
||||
|
||||
const usePrimitiveNodeKey = ({
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode = true,
|
||||
bindCurrentNode = false,
|
||||
children,
|
||||
heading = false,
|
||||
}: SemanticProps & { children?: ReactNode; heading?: boolean }) => {
|
||||
const data = useRender();
|
||||
const parentKey = useSemanticNodeKey();
|
||||
if (!bindSemanticNode) return undefined;
|
||||
if (bindCurrentNode) return parentKey;
|
||||
if (nodeKey) return nodeKey;
|
||||
if (semanticField && parentKey) return semanticNodeKeys.field(parentKey, semanticField);
|
||||
if (!parentKey) return undefined;
|
||||
|
||||
if (parentKey.endsWith("/header")) {
|
||||
if (heading && children === data.basics.name) return semanticNodeKeys.headerPart(parentKey, "name");
|
||||
if (!heading && children === data.basics.headline) return semanticNodeKeys.headerPart(parentKey, "headline");
|
||||
}
|
||||
|
||||
if (heading && parentKey.includes("/section-") && !parentKey.includes("/section-items")) {
|
||||
return semanticNodeKeys.sectionHeading(parentKey);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const Div = ({
|
||||
style,
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
bindCurrentNode,
|
||||
...props
|
||||
}: ComponentProps<typeof View> & SemanticProps) => {
|
||||
const divStyle = useTemplateStyle("div");
|
||||
|
||||
return <View style={composeStyles(divStyle, style as Style | Style[] | undefined)} {...props} />;
|
||||
};
|
||||
|
||||
export const Text = ({ style, ...props }: ComponentProps<typeof PdfText>) => {
|
||||
const textStyle = useTemplateStyle("text");
|
||||
const textRuleStyle = useSectionStyleRule("text");
|
||||
|
||||
return <PdfText style={composeStyles(textStyle, textRuleStyle, asStyleInput(style), safeTextStyle)} {...props} />;
|
||||
};
|
||||
|
||||
export const Heading = ({ style, ...props }: ComponentProps<typeof PdfText>) => {
|
||||
const headingStyle = useTemplateStyle("heading");
|
||||
const headingRuleStyle = useSectionStyleRule("heading");
|
||||
const resolvedNodeKey = usePrimitiveNodeKey({
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
bindCurrentNode,
|
||||
children: getChildren(props),
|
||||
});
|
||||
const resolved = useResolvedNode(resolvedNodeKey);
|
||||
const visible = useSemanticNodeVisible(resolvedNodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<PdfText style={composeStyles(headingStyle, headingRuleStyle, asStyleInput(style), safeTextStyle)} {...props} />
|
||||
<View
|
||||
{...props}
|
||||
{...resolvedPdfFlowProps(resolved)}
|
||||
style={composeStyles(divStyle, style as Style | Style[] | undefined, resolved.style)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Link = ({ style, ...props }: ComponentProps<typeof PdfLink>) => {
|
||||
export const Text = ({
|
||||
style,
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
bindCurrentNode: _bindCurrentNode,
|
||||
...props
|
||||
}: ComponentProps<typeof PdfText> & SemanticProps) => {
|
||||
const textStyle = useTemplateStyle("text");
|
||||
const textRuleStyle = useSectionStyleRule("text");
|
||||
const resolvedNodeKey = usePrimitiveNodeKey({
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
children: getChildren(props),
|
||||
});
|
||||
const resolved = useResolvedNode(resolvedNodeKey);
|
||||
const visible = useSemanticNodeVisible(resolvedNodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<PdfText
|
||||
{...props}
|
||||
{...resolvedPdfTextProps(resolved)}
|
||||
style={composeStyles(textStyle, textRuleStyle, asStyleInput(style), resolved.style, safeTextStyle)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Heading = ({
|
||||
style,
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
bindCurrentNode: _bindCurrentNode,
|
||||
...props
|
||||
}: ComponentProps<typeof PdfText> & SemanticProps) => {
|
||||
const headingStyle = useTemplateStyle("heading");
|
||||
const headingRuleStyle = useSectionStyleRule("heading");
|
||||
const resolvedNodeKey = usePrimitiveNodeKey({
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
children: getChildren(props),
|
||||
heading: true,
|
||||
});
|
||||
const resolved = useResolvedNode(resolvedNodeKey);
|
||||
const visible = useSemanticNodeVisible(resolvedNodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<PdfText
|
||||
{...props}
|
||||
{...resolvedPdfTextProps(resolved)}
|
||||
style={composeStyles(headingStyle, headingRuleStyle, asStyleInput(style), resolved.style, safeTextStyle)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Link = ({
|
||||
style,
|
||||
nodeKey,
|
||||
semanticField,
|
||||
semanticRole,
|
||||
bindSemanticNode: _bindSemanticNode,
|
||||
bindCurrentNode: _bindCurrentNode,
|
||||
...props
|
||||
}: ComponentProps<typeof PdfLink> & SemanticLinkProps) => {
|
||||
const { metadata } = useRender();
|
||||
const linkStyle = useTemplateStyle("link");
|
||||
const linkRuleStyle = useSectionStyleRule("link");
|
||||
const parentKey = useSemanticNodeKey();
|
||||
const resolvedNodeKey =
|
||||
nodeKey ??
|
||||
(semanticRole && parentKey ? semanticNodeKeys.link(parentKey, semanticRole) : undefined) ??
|
||||
(semanticField && parentKey ? semanticNodeKeys.field(parentKey, semanticField) : undefined);
|
||||
const resolved = useResolvedNode(resolvedNodeKey);
|
||||
const visible = useSemanticNodeVisible(resolvedNodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<PdfLink
|
||||
style={composeLinkStyles(
|
||||
{ hideUnderline: metadata.page.hideLinkUnderline },
|
||||
linkStyle,
|
||||
linkRuleStyle,
|
||||
asStyleInput(style),
|
||||
{...props}
|
||||
{...resolvedPdfTextProps(resolved)}
|
||||
style={composeStyles(
|
||||
composeLinkStyles(
|
||||
{ hideUnderline: metadata.page.hideLinkUnderline },
|
||||
linkStyle,
|
||||
linkRuleStyle,
|
||||
asStyleInput(style),
|
||||
),
|
||||
resolved.style,
|
||||
safeTextStyle,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Small = ({ style, ...props }: ComponentProps<typeof PdfText>) => {
|
||||
export const Small = ({
|
||||
style,
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
bindCurrentNode: _bindCurrentNode,
|
||||
...props
|
||||
}: ComponentProps<typeof PdfText> & SemanticProps) => {
|
||||
const textStyle = useTemplateStyle("text");
|
||||
const smallStyle = useTemplateStyle("small");
|
||||
const secondaryTextRuleStyle = useSectionStyleRule("secondaryText");
|
||||
const resolvedNodeKey = usePrimitiveNodeKey({
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
children: getChildren(props),
|
||||
});
|
||||
const resolved = useResolvedNode(resolvedNodeKey);
|
||||
const visible = useSemanticNodeVisible(resolvedNodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<PdfText
|
||||
style={composeStyles(textStyle, smallStyle, secondaryTextRuleStyle, asStyleInput(style), safeTextStyle)}
|
||||
{...props}
|
||||
{...resolvedPdfTextProps(resolved)}
|
||||
style={composeStyles(
|
||||
textStyle,
|
||||
smallStyle,
|
||||
secondaryTextRuleStyle,
|
||||
asStyleInput(style),
|
||||
resolved.style,
|
||||
safeTextStyle,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Bold = ({ style, ...props }: ComponentProps<typeof PdfText>) => {
|
||||
export const Bold = ({
|
||||
style,
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
bindCurrentNode: _bindCurrentNode,
|
||||
...props
|
||||
}: ComponentProps<typeof PdfText> & SemanticProps) => {
|
||||
const textStyle = useTemplateStyle("text");
|
||||
const boldStyle = useTemplateStyle("bold");
|
||||
const textRuleStyle = useSectionStyleRule("text");
|
||||
const resolvedNodeKey = usePrimitiveNodeKey({
|
||||
nodeKey,
|
||||
semanticField,
|
||||
bindSemanticNode,
|
||||
children: getChildren(props),
|
||||
});
|
||||
const resolved = useResolvedNode(resolvedNodeKey);
|
||||
const visible = useSemanticNodeVisible(resolvedNodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<PdfText
|
||||
style={composeStyles(textStyle, textRuleStyle, boldStyle, asStyleInput(style), safeTextStyle)}
|
||||
{...props}
|
||||
{...resolvedPdfTextProps(resolved)}
|
||||
style={composeStyles(textStyle, textRuleStyle, boldStyle, asStyleInput(style), resolved.style, safeTextStyle)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Icon = ({ style, size: sizeProp, ...props }: ComponentProps<typeof PhosphorIcon>) => {
|
||||
export const Icon = ({
|
||||
style,
|
||||
size: sizeProp,
|
||||
nodeKey,
|
||||
...props
|
||||
}: ComponentProps<typeof PhosphorIcon> & { nodeKey?: string | undefined }) => {
|
||||
const { style: iconStyle, size: templateSize, ...iconProps } = useTemplateIconSlot("icon");
|
||||
const iconRuleStyle = useSectionStyleRule("icon");
|
||||
const composedStyle = composeStyles(asStyleInput(iconStyle), iconRuleStyle, asStyleInput(style));
|
||||
const templateIconSize =
|
||||
typeof templateSize === "number" || typeof templateSize === "string" ? templateSize : undefined;
|
||||
const parentKey = useSemanticNodeKey();
|
||||
const resolvedNodeKey = nodeKey ?? (parentKey ? semanticNodeKeys.icon(parentKey, "item") : undefined);
|
||||
const resolved = useResolvedNode(resolvedNodeKey);
|
||||
const visible = useSemanticNodeVisible(resolvedNodeKey);
|
||||
const resolvedSize =
|
||||
resolveIconSize({
|
||||
size: sizeProp,
|
||||
styles: [iconRuleStyle, asStyleInput(style)],
|
||||
styles: [iconRuleStyle, asStyleInput(style), resolved.style],
|
||||
}) ?? templateIconSize;
|
||||
|
||||
if (iconProps.display === "none") return null;
|
||||
if (iconProps.display === "none" || !visible) return null;
|
||||
|
||||
return (
|
||||
<PhosphorIcon
|
||||
{...iconProps}
|
||||
{...props}
|
||||
{...(resolvedSize === undefined ? {} : { size: resolvedSize })}
|
||||
style={composedStyle}
|
||||
style={composeStyles(composedStyle, resolved.style)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SectionHeadingIcon = ({ style, size: sizeProp, ...props }: ComponentProps<typeof PhosphorIcon>) => {
|
||||
export const SemanticHeaderView = ({ style, ...props }: ComponentProps<typeof View>) => {
|
||||
const pageNodeKey = useTemplatePageNodeKey();
|
||||
const regionNodeKey = semanticNodeKeys.region(pageNodeKey, "header");
|
||||
const nodeKey = semanticNodeKeys.header(regionNodeKey);
|
||||
const regionResolved = useResolvedNode(regionNodeKey);
|
||||
const resolved = useResolvedNode(nodeKey);
|
||||
const regionVisible = useSemanticNodeVisible(regionNodeKey);
|
||||
const headerVisible = useSemanticNodeVisible(nodeKey);
|
||||
if (!regionVisible || !headerVisible) return null;
|
||||
|
||||
return (
|
||||
<SemanticNodeKeyProvider nodeKey={nodeKey}>
|
||||
<View
|
||||
{...props}
|
||||
{...resolvedPdfFlowProps(regionResolved)}
|
||||
{...resolvedPdfFlowProps(resolved)}
|
||||
style={composeStyles(asStyleInput(style), regionResolved.style, resolved.style)}
|
||||
/>
|
||||
</SemanticNodeKeyProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const SemanticRegionView = ({ region, style, ...props }: ComponentProps<typeof View> & { region: string }) => {
|
||||
const pageNodeKey = useTemplatePageNodeKey();
|
||||
const nodeKey = semanticNodeKeys.region(pageNodeKey, region);
|
||||
const resolved = useResolvedNode(nodeKey);
|
||||
const exists = useSemanticNodeExists(nodeKey);
|
||||
const visible = useSemanticNodeVisible(nodeKey);
|
||||
if (exists && !visible) return null;
|
||||
|
||||
return (
|
||||
<View {...props} {...resolvedPdfFlowProps(resolved)} style={composeStyles(asStyleInput(style), resolved.style)} />
|
||||
);
|
||||
};
|
||||
|
||||
export const SemanticRegionTemplatePartView = ({
|
||||
region,
|
||||
partKeys,
|
||||
style,
|
||||
...props
|
||||
}: ComponentProps<typeof View> & { region: string; partKeys: readonly string[] }) => {
|
||||
const pageNodeKey = useTemplatePageNodeKey();
|
||||
const regionNodeKey = semanticNodeKeys.region(pageNodeKey, region);
|
||||
const partNodeKey = semanticTemplatePartNodeKey(regionNodeKey, ...partKeys);
|
||||
const regionResolved = useResolvedNode(regionNodeKey);
|
||||
const partResolved = useResolvedNode(partNodeKey);
|
||||
const regionVisible = useSemanticNodeVisible(regionNodeKey);
|
||||
const partVisible = useSemanticNodeVisible(partNodeKey);
|
||||
if (!regionVisible || !partVisible) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...props}
|
||||
{...resolvedPdfFlowProps(regionResolved)}
|
||||
{...resolvedPdfFlowProps(partResolved)}
|
||||
style={composeStyles(asStyleInput(style), regionResolved.style, partResolved.style)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SemanticContactListView = ({ style, ...props }: ComponentProps<typeof View>) => {
|
||||
const headerNodeKey = useSemanticNodeKey();
|
||||
const nodeKey = headerNodeKey ? semanticNodeKeys.contactList(headerNodeKey) : undefined;
|
||||
const resolved = useResolvedNode(nodeKey);
|
||||
const renderedChildKeys = useRenderedChildKeys(nodeKey);
|
||||
const visible = useSemanticNodeVisible(nodeKey);
|
||||
if (!visible) return null;
|
||||
const children =
|
||||
nodeKey && renderedChildKeys ? projectContactChildren(props.children, nodeKey, renderedChildKeys) : props.children;
|
||||
|
||||
return (
|
||||
<View {...props} {...resolvedPdfFlowProps(resolved)} style={composeStyles(asStyleInput(style), resolved.style)}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const SemanticContactRowView = ({
|
||||
partKey,
|
||||
style,
|
||||
...props
|
||||
}: ComponentProps<typeof View> & { partKey: string }) => {
|
||||
const headerNodeKey = useSemanticNodeKey();
|
||||
const contactListNodeKey = headerNodeKey ? semanticNodeKeys.contactList(headerNodeKey) : undefined;
|
||||
const nodeKey = semanticTemplatePartNodeKey(contactListNodeKey, partKey);
|
||||
const resolved = useResolvedNode(nodeKey);
|
||||
const renderedChildKeys = useRenderedChildKeys(nodeKey);
|
||||
const visible = useSemanticNodeVisible(nodeKey);
|
||||
if (!visible) return null;
|
||||
const children =
|
||||
contactListNodeKey && renderedChildKeys
|
||||
? projectContactChildren(props.children, contactListNodeKey, renderedChildKeys)
|
||||
: props.children;
|
||||
|
||||
return (
|
||||
<View {...props} {...resolvedPdfFlowProps(resolved)} style={composeStyles(asStyleInput(style), resolved.style)}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export function semanticTemplatePartNodeKey(ownerNodeKey: string | undefined, ...partKeys: string[]) {
|
||||
return ownerNodeKey ? partKeys.reduce((key, part) => `${key}/template-part-${part}`, ownerNodeKey) : undefined;
|
||||
}
|
||||
|
||||
export const SemanticTemplatePartView = ({
|
||||
ownerNodeKey,
|
||||
partKeys,
|
||||
style,
|
||||
...props
|
||||
}: ComponentProps<typeof View> & { ownerNodeKey?: string | undefined; partKeys: readonly string[] }) => {
|
||||
const contextualOwnerNodeKey = useSemanticNodeKey();
|
||||
const nodeKey = semanticTemplatePartNodeKey(ownerNodeKey ?? contextualOwnerNodeKey, ...partKeys);
|
||||
const resolved = useResolvedNode(nodeKey);
|
||||
const visible = useSemanticNodeVisible(nodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<View {...props} {...resolvedPdfFlowProps(resolved)} style={composeStyles(asStyleInput(style), resolved.style)} />
|
||||
);
|
||||
};
|
||||
|
||||
export const SemanticHeaderPicture = ({ style, ...props }: ComponentProps<typeof Image>) => {
|
||||
const pageNodeKey = useTemplatePageNodeKey();
|
||||
const headerNodeKey = useSemanticNodeKey() ?? semanticNodeKeys.header(semanticNodeKeys.region(pageNodeKey, "header"));
|
||||
const nodeKey = headerNodeKey ? semanticNodeKeys.headerPart(headerNodeKey, "picture") : undefined;
|
||||
const resolved = useResolvedNode(nodeKey);
|
||||
const visible = useSemanticNodeVisible(nodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return <Image {...props} style={composeStyles(asStyleInput(style), resolved.style)} />;
|
||||
};
|
||||
|
||||
export const SectionHeadingIcon = ({
|
||||
style,
|
||||
size: sizeProp,
|
||||
nodeKey,
|
||||
...props
|
||||
}: ComponentProps<typeof PhosphorIcon> & { nodeKey?: string | undefined }) => {
|
||||
const data = useRender();
|
||||
const { style: sectionIconStyle, ...sectionIconProps } = useTemplateIconSlot("sectionHeadingIcon");
|
||||
const { style: fallbackIconStyle, ...fallbackIconProps } = useTemplateIconSlot("icon");
|
||||
@@ -127,13 +484,16 @@ export const SectionHeadingIcon = ({ style, size: sizeProp, ...props }: Componen
|
||||
}) ??
|
||||
templateIconSize ??
|
||||
headingFontSize;
|
||||
const resolved = useResolvedNode(nodeKey);
|
||||
const visible = useSemanticNodeVisible(nodeKey);
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<PhosphorIcon
|
||||
{...iconPropsWithoutDisplay}
|
||||
{...props}
|
||||
{...(resolvedSize === undefined ? {} : { size: resolvedSize })}
|
||||
style={composeStyles(asStyleInput(iconStyle), asStyleInput(style))}
|
||||
style={composeStyles(asStyleInput(iconStyle), asStyleInput(style), resolved.style)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -109,6 +109,10 @@ describe("normalizeRichTextHtml", () => {
|
||||
it("does not double-wrap inline tags inside block elements", () => {
|
||||
expect(normalizeRichTextHtml("<p><strong>x</strong></p>")).toBe("<p><strong>x</strong></p>");
|
||||
});
|
||||
|
||||
it("normalizes RTL pseudo-bullets into anchored list items in the shared HTML path", () => {
|
||||
expect(normalizeRichTextHtml("<p>- א<br>- ב</p>", { direction: "rtl" })).toBe("<ul><li>א</li><li>ב</li></ul>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertPseudoBulletParagraphs", () => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { HTMLElement, Node } from "node-html-parser";
|
||||
import { NodeType, parse } from "node-html-parser";
|
||||
import { isDarkColor } from "@reactive-resume/utils/color";
|
||||
import { getRichTextSemanticKind, getRichTextSemanticNodeKey } from "../../semantic/rich-text-keys";
|
||||
|
||||
export const richTextMarkClassName = "rr-pdf-mark";
|
||||
export const richTextSemanticNodeKeyAttribute = "data-resume-semantic-node-key";
|
||||
|
||||
const inlineTags = new Set([
|
||||
"a",
|
||||
@@ -121,7 +123,14 @@ export const convertPseudoBulletParagraphs = (html: string): string =>
|
||||
return converted ?? full;
|
||||
});
|
||||
|
||||
export const normalizeRichTextHtml = (html: string): string => {
|
||||
type NormalizeRichTextHtmlOptions = {
|
||||
direction?: "ltr" | "rtl";
|
||||
};
|
||||
|
||||
export const normalizeRichTextHtml = (
|
||||
html: string,
|
||||
{ direction = "ltr" }: NormalizeRichTextHtmlOptions = {},
|
||||
): string => {
|
||||
const root = parse(html.trim(), { comment: false });
|
||||
const normalized: string[] = [];
|
||||
let inlineNodes: string[] = [];
|
||||
@@ -151,5 +160,71 @@ export const normalizeRichTextHtml = (html: string): string => {
|
||||
|
||||
flushInlineNodes();
|
||||
|
||||
return normalized.join("");
|
||||
const normalizedHtml = normalized.join("");
|
||||
if (direction !== "rtl") return normalizedHtml;
|
||||
|
||||
// RTL pseudo-bullets must become real list items before both the semantic
|
||||
// descriptor and renderer traverse the HTML. RLM anchors each independent
|
||||
// react-pdf-html text frame without changing element ancestry or indices.
|
||||
return convertPseudoBulletParagraphs(normalizedHtml).replace(
|
||||
/<(p|li)\b([^>]*)>/gi,
|
||||
(_match, tag, rest) => `<${tag}${rest}>`,
|
||||
);
|
||||
};
|
||||
|
||||
export const parseNormalizedRichTextHtml = (html: string, options?: NormalizeRichTextHtmlOptions) =>
|
||||
parse(normalizeRichTextHtml(html, options), { comment: false });
|
||||
|
||||
export const projectNormalizedRichTextHtml = (
|
||||
html: string,
|
||||
rootNodeKey: string,
|
||||
renderedChildKeysFor: (nodeKey: string) => readonly string[] | undefined,
|
||||
): string => {
|
||||
const root = parse(html, { comment: false });
|
||||
const elements = root.querySelectorAll("*");
|
||||
|
||||
for (const element of elements) {
|
||||
if (!getRichTextSemanticKind(element, richTextMarkClassName)) continue;
|
||||
element.setAttribute(
|
||||
richTextSemanticNodeKeyAttribute,
|
||||
getRichTextSemanticNodeKey(rootNodeKey, element, richTextMarkClassName),
|
||||
);
|
||||
}
|
||||
|
||||
const visit = (parent: HTMLElement, parentNodeKey: string) => {
|
||||
for (const child of parent.childNodes) {
|
||||
if (!isElement(child)) continue;
|
||||
const childNodeKey = child.getAttribute(richTextSemanticNodeKeyAttribute);
|
||||
const childContentNodeKey =
|
||||
childNodeKey && getTagName(child) === "li"
|
||||
? `${childNodeKey}/list-item-content-0`
|
||||
: (childNodeKey ?? parentNodeKey);
|
||||
visit(child, childContentNodeKey);
|
||||
}
|
||||
|
||||
const keyedChildren = parent.childNodes.flatMap((child) => {
|
||||
if (!isElement(child)) return [];
|
||||
const nodeKey = child.getAttribute(richTextSemanticNodeKeyAttribute);
|
||||
return nodeKey ? [{ nodeKey, child }] : [];
|
||||
});
|
||||
if (keyedChildren.length === 0) return;
|
||||
const renderedChildKeys = renderedChildKeysFor(parentNodeKey);
|
||||
if (!renderedChildKeys) return;
|
||||
|
||||
const childByNodeKey = new Map(keyedChildren.map(({ nodeKey, child }) => [nodeKey, child]));
|
||||
const projected = renderedChildKeys.flatMap((nodeKey) => {
|
||||
const child = childByNodeKey.get(nodeKey);
|
||||
return child ? [child] : [];
|
||||
});
|
||||
let projectedIndex = 0;
|
||||
const nextChildren = parent.childNodes.flatMap((child) => {
|
||||
if (!isElement(child) || !child.getAttribute(richTextSemanticNodeKeyAttribute)) return [child];
|
||||
const projectedChild = projected[projectedIndex++];
|
||||
return projectedChild ? [projectedChild] : [];
|
||||
});
|
||||
parent.set_content(nextChildren);
|
||||
};
|
||||
|
||||
visit(root, rootNodeKey);
|
||||
return root.toString();
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isRichTextElementInsideListItem,
|
||||
stripRichTextVerticalMargins,
|
||||
} from "./rich-text-spacing";
|
||||
import { safeTextStyle } from "./safe-text-style";
|
||||
import { composeStyles } from "./styles";
|
||||
|
||||
export const toRichTextStyleArray = (style: Style | Style[] | undefined): Style[] => {
|
||||
@@ -20,18 +21,22 @@ type RichTextParagraphRendererProps = {
|
||||
children: ReactNode;
|
||||
element: Parameters<typeof isRichTextElementInsideListItem>[0];
|
||||
style: Style | Style[] | undefined;
|
||||
semanticStyle?: Style | Style[] | undefined;
|
||||
rtl?: boolean;
|
||||
rtlTextWrapStyle?: Style | undefined;
|
||||
applyRtlDirection?: (node: ReactNode) => ReactNode;
|
||||
textProps?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const renderRichTextParagraph = ({
|
||||
element,
|
||||
style,
|
||||
semanticStyle,
|
||||
children,
|
||||
rtl,
|
||||
rtlTextWrapStyle,
|
||||
applyRtlDirection,
|
||||
textProps,
|
||||
}: RichTextParagraphRendererProps) => {
|
||||
const paragraphStyles = isRichTextElementInsideListItem(element)
|
||||
? toRichTextStyleArray(style).map(stripRichTextVerticalMargins)
|
||||
@@ -41,9 +46,11 @@ export const renderRichTextParagraph = ({
|
||||
paragraphStyles,
|
||||
getRichTextEdgeTrimStyle(element),
|
||||
rtl ? rtlTextWrapStyle : undefined,
|
||||
semanticStyle,
|
||||
safeTextStyle,
|
||||
);
|
||||
|
||||
const content = rtl && applyRtlDirection ? applyRtlDirection(children) : children;
|
||||
|
||||
return createElement(PdfText, { style: composedStyle }, content);
|
||||
return createElement(PdfText, { ...textProps, style: composedStyle }, content);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ import { createRichTextStylesheet } from "./rich-text-stylesheet";
|
||||
type PdfElement = ReactElement<{ children?: unknown; style?: unknown }>;
|
||||
|
||||
const getPdfElementProps = (element: unknown) => (element as PdfElement).props;
|
||||
const mergeStyle = (style: unknown): Record<string, unknown> =>
|
||||
Object.assign({}, ...(Array.isArray(style) ? style : style ? [style] : []));
|
||||
|
||||
describe("normalizeRichTextHtml", () => {
|
||||
it("wraps top-level inline rich text in a paragraph", () => {
|
||||
@@ -50,6 +52,33 @@ describe("renderRichTextParagraph", () => {
|
||||
expect(rendered.type).toBe(PdfText);
|
||||
expect(props.children).toEqual(["Plain ", expect.any(Object), " text"]);
|
||||
});
|
||||
|
||||
it("keeps semantic paragraph margins after prose edge trimming while preserving text safety", () => {
|
||||
const paragraph = parse("<p>First</p><p>Second</p>").querySelector("p");
|
||||
|
||||
if (!paragraph) throw new Error("Expected paragraph to exist.");
|
||||
|
||||
const rendered = renderRichTextParagraph({
|
||||
element: paragraph,
|
||||
style: { marginTop: 4, marginBottom: 4 },
|
||||
semanticStyle: {
|
||||
marginTop: 20,
|
||||
marginBottom: 21,
|
||||
minWidth: 72,
|
||||
maxWidth: 72,
|
||||
flexShrink: 0,
|
||||
},
|
||||
children: "First",
|
||||
});
|
||||
|
||||
expect(mergeStyle(getPdfElementProps(rendered).style)).toMatchObject({
|
||||
marginTop: 20,
|
||||
marginBottom: 21,
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
flexShrink: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createRichTextStylesheet", () => {
|
||||
|
||||
@@ -2,10 +2,25 @@ 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 { Text as PdfText, View } from "#react-pdf-renderer";
|
||||
import { Link as PdfLink, Text as PdfText, View } from "#react-pdf-renderer";
|
||||
import { useRender } from "../../context";
|
||||
import { resolvedPdfFlowProps, resolvedPdfTextProps } from "../../semantic/adapter";
|
||||
import {
|
||||
projectRenderedChildren,
|
||||
useResolvedNode,
|
||||
useSemanticNodeBindings,
|
||||
useSemanticNodeKey,
|
||||
useSemanticNodeVisible,
|
||||
} from "../../semantic/context";
|
||||
import { semanticNodeKeys } from "../../semantic/node-keys";
|
||||
import { getRichTextSemanticNodeKey } from "../../semantic/rich-text-keys";
|
||||
import { useSectionStyleRule, useTemplateStyle } from "./context";
|
||||
import { convertPseudoBulletParagraphs, normalizeRichTextHtml } from "./rich-text-html";
|
||||
import {
|
||||
normalizeRichTextHtml,
|
||||
projectNormalizedRichTextHtml,
|
||||
richTextMarkClassName,
|
||||
richTextSemanticNodeKeyAttribute,
|
||||
} from "./rich-text-html";
|
||||
import { renderRichTextParagraph, toRichTextStyleArray } from "./rich-text-renderers";
|
||||
import {
|
||||
createRichTextProseSpacing,
|
||||
@@ -24,6 +39,7 @@ const richListItemContentStackStyle = {
|
||||
|
||||
type RichTextProps = {
|
||||
children: string;
|
||||
semanticField?: string | undefined;
|
||||
};
|
||||
|
||||
// react-pdf textkit reads BiDi base direction from each run's own `direction` attribute
|
||||
@@ -57,8 +73,18 @@ const applyRtlDirectionRecursively = (node: ReactNode): ReactNode => {
|
||||
return cloneElement(element, { style: nextStyle }, nextChildren);
|
||||
};
|
||||
|
||||
export const RichText = ({ children }: RichTextProps) => {
|
||||
export const RichText = ({ children, semanticField }: RichTextProps) => {
|
||||
const { metadata, rtl } = useRender();
|
||||
const parentNodeKey = useSemanticNodeKey();
|
||||
const fieldNodeKey =
|
||||
parentNodeKey && semanticField ? semanticNodeKeys.field(parentNodeKey, semanticField) : undefined;
|
||||
const richTextNodeKey =
|
||||
fieldNodeKey && semanticField ? semanticNodeKeys.richText(fieldNodeKey, semanticField) : undefined;
|
||||
const fieldResolved = useResolvedNode(fieldNodeKey);
|
||||
const fieldVisible = useSemanticNodeVisible(fieldNodeKey);
|
||||
const richTextResolved = useResolvedNode(richTextNodeKey);
|
||||
const richTextVisible = useSemanticNodeVisible(richTextNodeKey);
|
||||
const { resolveNode, isNodeVisible, renderedChildKeysFor } = useSemanticNodeBindings();
|
||||
const rtlTextWrapStyle: Style | undefined = rtl ? { direction: "rtl", textAlign: "right" } : undefined;
|
||||
|
||||
const boldStyle = useTemplateStyle("bold");
|
||||
@@ -82,29 +108,120 @@ export const RichText = ({ children }: RichTextProps) => {
|
||||
);
|
||||
const proseSpacing = createRichTextProseSpacing(bodyLineHeight);
|
||||
|
||||
const normalized = normalizeRichTextHtml(children);
|
||||
// RTL-only: pseudo-bullets share one BiDi paragraph across <br>, causing chars to
|
||||
// bleed between visual lines. Real <li> items are independent BiDi paragraphs.
|
||||
const withBullets = normalized && rtl ? convertPseudoBulletParagraphs(normalized) : normalized;
|
||||
// Inject U+200F (RLM) after each <p>/<li> opener to anchor BiDi base direction
|
||||
// on the inner styleless <Text> frame react-pdf-html creates.
|
||||
const html =
|
||||
withBullets && rtl
|
||||
? withBullets.replace(/<(p|li)\b([^>]*)>/gi, (_match, tag, rest) => `<${tag}${rest}>`)
|
||||
: withBullets;
|
||||
const normalizedHtml = normalizeRichTextHtml(children, { direction: rtl ? "rtl" : "ltr" });
|
||||
const html = richTextNodeKey
|
||||
? projectNormalizedRichTextHtml(normalizedHtml, richTextNodeKey, renderedChildKeysFor)
|
||||
: normalizedHtml;
|
||||
|
||||
if (!html) return null;
|
||||
if (!html || !fieldVisible || !richTextVisible) return null;
|
||||
|
||||
const keyFor = (element: Parameters<typeof getRichTextSemanticNodeKey>[1]) =>
|
||||
element.getAttribute(richTextSemanticNodeKeyAttribute) ??
|
||||
(richTextNodeKey ? getRichTextSemanticNodeKey(richTextNodeKey, element, richTextMarkClassName) : undefined);
|
||||
const resolvedFor = (element: Parameters<typeof getRichTextSemanticNodeKey>[1]) => resolveNode(keyFor(element));
|
||||
const renderText = ({
|
||||
element,
|
||||
style,
|
||||
children: textChildren,
|
||||
}: {
|
||||
element: Parameters<typeof getRichTextSemanticNodeKey>[1];
|
||||
style: Style[];
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const nodeKey = keyFor(element);
|
||||
const resolved = resolveNode(nodeKey);
|
||||
const visible = isNodeVisible(nodeKey);
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<PdfText {...resolvedPdfTextProps(resolved)} style={composeStyles(style, resolved.style, safeTextStyle)}>
|
||||
{textChildren}
|
||||
</PdfText>
|
||||
);
|
||||
};
|
||||
const renderView = ({
|
||||
element,
|
||||
style,
|
||||
children: viewChildren,
|
||||
}: {
|
||||
element: Parameters<typeof getRichTextSemanticNodeKey>[1];
|
||||
style: Style[];
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const nodeKey = keyFor(element);
|
||||
const resolved = resolveNode(nodeKey);
|
||||
const visible = isNodeVisible(nodeKey);
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<View {...resolvedPdfFlowProps(resolved)} style={composeStyles(style, resolved.style)}>
|
||||
{viewChildren}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Html
|
||||
resetStyles
|
||||
{...resolvedPdfFlowProps(richTextResolved)}
|
||||
style={composeStyles(fieldResolved.style, richTextResolved.style)}
|
||||
renderers={{
|
||||
b: ({ children }) => (
|
||||
<PdfText style={composeStyles(boldStyle, richBoldRuleStyle, safeTextStyle)}>{children}</PdfText>
|
||||
),
|
||||
h1: renderText,
|
||||
h2: renderText,
|
||||
h3: renderText,
|
||||
h4: renderText,
|
||||
h5: renderText,
|
||||
h6: renderText,
|
||||
blockquote: renderView,
|
||||
ul: renderView,
|
||||
ol: renderView,
|
||||
b: renderText,
|
||||
strong: renderText,
|
||||
em: renderText,
|
||||
i: renderText,
|
||||
u: renderText,
|
||||
s: renderText,
|
||||
strike: renderText,
|
||||
code: renderText,
|
||||
span: renderText,
|
||||
mark: renderText,
|
||||
a: ({ element, style, children: linkChildren }) => {
|
||||
const nodeKey = keyFor(element);
|
||||
const resolved = resolvedFor(element);
|
||||
if (!isNodeVisible(nodeKey)) return null;
|
||||
return (
|
||||
<PdfLink
|
||||
{...resolvedPdfTextProps(resolved)}
|
||||
src={element.attributes.href ?? ""}
|
||||
style={composeStyles(style, resolved.style, safeTextStyle)}
|
||||
>
|
||||
{linkChildren}
|
||||
</PdfLink>
|
||||
);
|
||||
},
|
||||
br: ({ element, style }) => {
|
||||
const resolved = resolvedFor(element);
|
||||
return (
|
||||
<PdfText
|
||||
{...resolvedPdfTextProps(resolved)}
|
||||
wrap={false}
|
||||
style={composeStyles(style, resolved.style, safeTextStyle)}
|
||||
>
|
||||
{"\n"}
|
||||
</PdfText>
|
||||
);
|
||||
},
|
||||
hr: ({ element, style }) => {
|
||||
const nodeKey = keyFor(element);
|
||||
const resolved = resolvedFor(element);
|
||||
if (!isNodeVisible(nodeKey)) return null;
|
||||
return <View {...resolvedPdfFlowProps(resolved)} style={composeStyles(style, resolved.style)} />;
|
||||
},
|
||||
p: (props) => {
|
||||
const resolved = resolvedFor(props.element);
|
||||
const paragraphProps = {
|
||||
...props,
|
||||
style: props.style,
|
||||
semanticStyle: resolved.style,
|
||||
textProps: resolvedPdfTextProps(resolved),
|
||||
rtl,
|
||||
...(rtlTextWrapStyle ? { rtlTextWrapStyle } : {}),
|
||||
...(rtl ? { applyRtlDirection: applyRtlDirectionRecursively } : {}),
|
||||
@@ -113,6 +230,16 @@ export const RichText = ({ children }: RichTextProps) => {
|
||||
return renderRichTextParagraph(paragraphProps);
|
||||
},
|
||||
li: ({ element, style, children }) => {
|
||||
const nodeKey = keyFor(element);
|
||||
const itemResolved = resolvedFor(element);
|
||||
if (!isNodeVisible(nodeKey)) return null;
|
||||
const itemNodeKey = nodeKey;
|
||||
const markerNodeKey = itemNodeKey ? semanticNodeKeys.richTextNode(itemNodeKey, "list-marker", 0) : undefined;
|
||||
const contentNodeKey = itemNodeKey
|
||||
? semanticNodeKeys.richTextNode(itemNodeKey, "list-item-content", 0)
|
||||
: undefined;
|
||||
const markerResolved = resolveNode(markerNodeKey);
|
||||
const contentResolved = resolveNode(contentNodeKey);
|
||||
const isOrderedList = isRichTextElementInsideOrderedList(element);
|
||||
const marker = isOrderedList ? `${element.indexOfType + 1}.` : "•";
|
||||
const itemStyles = toRichTextStyleArray(style);
|
||||
@@ -121,8 +248,11 @@ export const RichText = ({ children }: RichTextProps) => {
|
||||
const markerNode = (
|
||||
<PdfText
|
||||
key="marker"
|
||||
minPresenceAhead={bodyLineHeight ?? metadata.typography.body.lineHeight}
|
||||
style={composeStyles(richListItemMarkerStyle)}
|
||||
{...resolvedPdfTextProps(markerResolved)}
|
||||
minPresenceAhead={
|
||||
markerResolved.minPresenceAhead ?? bodyLineHeight ?? metadata.typography.body.lineHeight
|
||||
}
|
||||
style={composeStyles(richListItemMarkerStyle, markerResolved.style)}
|
||||
>
|
||||
{marker}
|
||||
</PdfText>
|
||||
@@ -132,10 +262,12 @@ export const RichText = ({ children }: RichTextProps) => {
|
||||
const contentNode = rtl ? (
|
||||
<PdfText
|
||||
key="content"
|
||||
{...resolvedPdfTextProps(contentResolved)}
|
||||
style={composeStyles(
|
||||
richListItemContentStyle,
|
||||
richListItemContentRuleStyle,
|
||||
contentItemStyles,
|
||||
contentResolved.style,
|
||||
safeTextStyle,
|
||||
rtlTextWrapStyle,
|
||||
)}
|
||||
@@ -145,10 +277,12 @@ export const RichText = ({ children }: RichTextProps) => {
|
||||
) : (
|
||||
<View
|
||||
key="content"
|
||||
{...resolvedPdfFlowProps(contentResolved)}
|
||||
style={composeStyles(
|
||||
richListItemContentStyle,
|
||||
richListItemContentRuleStyle,
|
||||
contentItemStyles,
|
||||
contentResolved.style,
|
||||
richListItemContentStackStyle,
|
||||
safeTextStyle,
|
||||
)}
|
||||
@@ -156,19 +290,34 @@ export const RichText = ({ children }: RichTextProps) => {
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
const authoredChildren = rtl
|
||||
? [
|
||||
{ nodeKey: contentNodeKey ?? "content", value: contentNode },
|
||||
{ nodeKey: markerNodeKey ?? "marker", value: markerNode },
|
||||
]
|
||||
: [
|
||||
{ nodeKey: markerNodeKey ?? "marker", value: markerNode },
|
||||
{ nodeKey: contentNodeKey ?? "content", value: contentNode },
|
||||
];
|
||||
const renderedChildren = projectRenderedChildren(
|
||||
renderedChildKeysFor(itemNodeKey),
|
||||
authoredChildren.filter(({ nodeKey }) => isNodeVisible(nodeKey)),
|
||||
);
|
||||
|
||||
// Yoga ignores `flexDirection`/`direction` on rows inside react-pdf-html's <ul>
|
||||
// (works fine for split-row/contact-list). Swap DOM order to position the marker.
|
||||
return (
|
||||
<View
|
||||
{...resolvedPdfFlowProps(itemResolved)}
|
||||
style={composeStyles(
|
||||
richListItemRowStyle,
|
||||
richListItemRowRuleStyle,
|
||||
itemStyles,
|
||||
getRichTextEdgeTrimStyle(element),
|
||||
itemResolved.style,
|
||||
)}
|
||||
>
|
||||
{rtl ? [contentNode, markerNode] : [markerNode, contentNode]}
|
||||
{renderedChildren}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -16,15 +16,20 @@ describe("ExperienceSection", () => {
|
||||
|
||||
describe("ItemTitle", () => {
|
||||
it("renders award titles without the bold style", () => {
|
||||
expect(source).toContain("const ItemTitle = ({ children, website, bold = true }: ItemTitleProps)");
|
||||
expect(source).toContain("const title = bold ? <Bold>{children}</Bold> : <Text>{children}</Text>;");
|
||||
expect(source).toContain("<ItemTitle website={item.website} bold={false}>");
|
||||
expect(source).toContain("const ItemTitle = ({ children, website, field, bold = true }: ItemTitleProps)");
|
||||
expect(source).toContain(
|
||||
"const title = bold ? <Bold semanticField={field}>{children}</Bold> : <Text semanticField={field}>{children}</Text>;",
|
||||
);
|
||||
expect(source).toContain('<ItemTitle field="title" website={item.website} bold={false}>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("SectionShell", () => {
|
||||
it("keeps section and heading style rules when section heading icons are hidden", () => {
|
||||
expect(source).toContain("<View style={composeStyles(sectionStyle, sectionRuleStyle)} {...breakProps}>");
|
||||
expect(source).toContain(
|
||||
"const resolvedSectionStyle = composeStyles(sectionStyle, sectionRuleStyle, resolved.style)",
|
||||
);
|
||||
expect(source).toContain("<View style={resolvedSectionStyle} {...flowProps}>");
|
||||
expect(source).toContain("<Heading style={composeStyles(sectionHeadingStyle, sectionHeadingRuleStyle)}>");
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user