mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-04 02:33:47 +10:00
feat(templates): add Porygon and Smeargle templates
Porygon lays the resume out as a ruled form after the Japanese résumé form (rirekisho). The header is a block of bordered field cells framed in the accent color with the photo in a cell of its own, and every section is a table with a solid title bar over one ruled cell per entry. Touching cells share one tinted rule. Smeargle sets the resume like a magazine feature: the headline as an uppercase kicker over a display name, the summary as an italic standfirst, and italic section headings at the user's heading size. Both are one-column and ATS-safe. Register them in the schema, renderer, semantic manifests and gallery, add gallery and docs previews, extract the new descriptions, regenerate the API spec, and list them wherever the docs enumerate templates.
This commit is contained in:
@@ -15,8 +15,10 @@ import { leafishSemanticManifest } from "../templates/leafish/semantic";
|
||||
import { meowthSemanticManifest } from "../templates/meowth/semantic";
|
||||
import { onyxSemanticManifest } from "../templates/onyx/semantic";
|
||||
import { pikachuSemanticManifest } from "../templates/pikachu/semantic";
|
||||
import { porygonSemanticManifest } from "../templates/porygon/semantic";
|
||||
import { rhyhornSemanticManifest } from "../templates/rhyhorn/semantic";
|
||||
import { scizorSemanticManifest } from "../templates/scizor/semantic";
|
||||
import { smeargleSemanticManifest } from "../templates/smeargle/semantic";
|
||||
import { SHARED_BINDING_REGISTRY } from "./binding-inventory";
|
||||
|
||||
export type TemplateSemanticPlacement = "main" | "sidebar";
|
||||
@@ -145,8 +147,10 @@ const TEMPLATE_SEMANTIC_MANIFESTS = {
|
||||
meowth: meowthSemanticManifest,
|
||||
onyx: onyxSemanticManifest,
|
||||
pikachu: pikachuSemanticManifest,
|
||||
porygon: porygonSemanticManifest,
|
||||
rhyhorn: rhyhornSemanticManifest,
|
||||
scizor: scizorSemanticManifest,
|
||||
smeargle: smeargleSemanticManifest,
|
||||
} as const satisfies Readonly<Record<Template, TemplateSemanticManifest>>;
|
||||
|
||||
export function getTemplateSemanticManifest(template: Template): TemplateSemanticManifest {
|
||||
|
||||
@@ -13,8 +13,10 @@ import { LeafishPage } from "./leafish/LeafishPage";
|
||||
import { MeowthPage } from "./meowth/MeowthPage";
|
||||
import { OnyxPage } from "./onyx/OnyxPage";
|
||||
import { PikachuPage } from "./pikachu/PikachuPage";
|
||||
import { PorygonPage } from "./porygon/PorygonPage";
|
||||
import { RhyhornPage } from "./rhyhorn/RhyhornPage";
|
||||
import { ScizorPage } from "./scizor/ScizorPage";
|
||||
import { SmearglePage } from "./smeargle/SmearglePage";
|
||||
|
||||
const templatePages: Partial<Record<Template, TemplatePage>> = {
|
||||
azurill: AzurillPage,
|
||||
@@ -30,8 +32,10 @@ const templatePages: Partial<Record<Template, TemplatePage>> = {
|
||||
meowth: MeowthPage,
|
||||
onyx: OnyxPage,
|
||||
pikachu: PikachuPage,
|
||||
porygon: PorygonPage,
|
||||
rhyhorn: RhyhornPage,
|
||||
scizor: ScizorPage,
|
||||
smeargle: SmearglePage,
|
||||
};
|
||||
|
||||
const defaultTemplatePage = AzurillPage;
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { Style } from "../../forme/style-types";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { useMemo } from "react";
|
||||
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 { getPrimaryTint } from "../shared/color-helpers";
|
||||
import {
|
||||
CustomFieldContactItem,
|
||||
EmailContactItem,
|
||||
LocationContactItem,
|
||||
PhoneContactItem,
|
||||
WebsiteContactItem,
|
||||
} from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles, headerNameLineHeight } from "../shared/styles";
|
||||
import { createIconSlot, useTemplateBase } from "../shared/template-base";
|
||||
|
||||
type PorygonStyles = Omit<TemplateStyleSlots, "page"> & {
|
||||
page: Style;
|
||||
header: Style;
|
||||
headerFields: Style;
|
||||
headerIdentity: Style;
|
||||
headerName: Style;
|
||||
headerHeadline: Style;
|
||||
contactList: Style;
|
||||
contactItem: Style;
|
||||
pictureCell: Style;
|
||||
picture: Style;
|
||||
sections: Style;
|
||||
};
|
||||
|
||||
type PorygonTemplate = {
|
||||
colors: TemplateColorRoles;
|
||||
styles: PorygonStyles;
|
||||
};
|
||||
|
||||
type PorygonHeaderProps = {
|
||||
styles: PorygonStyles;
|
||||
};
|
||||
|
||||
/**
|
||||
* Porygon lays the resume out as a ruled form, after the Japanese rirekisho: the header is a bordered
|
||||
* block of field cells with the photo in a cell of its own, and every section is a table with a solid
|
||||
* title bar over one ruled row per entry. The rules, not whitespace, carry the structure.
|
||||
*/
|
||||
export const PorygonPage = ({ 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 } = usePorygonTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = useRenderedSectionIds(pageNodeKey, filterSections(page.main, data));
|
||||
const sidebarSections = useRenderedSectionIds(pageNodeKey, page.fullWidth ? [] : filterSections(page.sidebar, data));
|
||||
const sections = [...mainSections, ...sidebarSections];
|
||||
|
||||
return (
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sections, { rowGap: metrics.sectionGap })}>
|
||||
{sections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</SemanticRegionView>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ styles }: PorygonHeaderProps) => {
|
||||
const { basics, picture } = useRender();
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
<View style={styles.headerFields}>
|
||||
<View style={styles.headerIdentity}>
|
||||
<Heading style={styles.headerName}>{basics.name}</Heading>
|
||||
{basics.headline && <Text style={styles.headerHeadline}>{basics.headline}</Text>}
|
||||
</View>
|
||||
|
||||
<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} />
|
||||
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
))}
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
|
||||
{hasPicture && (
|
||||
<View style={styles.pictureCell}>
|
||||
<SemanticHeaderPicture src={picture.url} style={styles.picture} />
|
||||
</View>
|
||||
)}
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
const usePorygonTemplate = (): PorygonTemplate => {
|
||||
const { metadata, r, foreground, background, primary, metrics, base } = useTemplateBase();
|
||||
|
||||
return useMemo(() => {
|
||||
const colors: TemplateColorRoles = { foreground, background, primary };
|
||||
const { heading } = metadata.typography;
|
||||
// The header's frame and the title bars are solid accent; every rule between cells is a light tint of it.
|
||||
const rule = getPrimaryTint(metadata.design.colors.primary, 0.3);
|
||||
const ruleWidth = 0.75;
|
||||
const cellX = metrics.gapX(0.75);
|
||||
const cellY = metrics.gapY(0.75);
|
||||
// Each contact cell draws the rule on its own start side; pulling the grid back by one rule hides the first
|
||||
// column's rule under the header's frame.
|
||||
const ruleStart = r.rtl
|
||||
? { borderRightWidth: ruleWidth, borderRightColor: rule }
|
||||
: { borderLeftWidth: ruleWidth, borderLeftColor: rule };
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
...base,
|
||||
page: {
|
||||
...base.page,
|
||||
paddingHorizontal: metrics.page.paddingHorizontal,
|
||||
paddingVertical: metrics.page.paddingVertical,
|
||||
rowGap: metrics.sectionGap,
|
||||
},
|
||||
heading: { ...base.heading, fontWeight: heading.fontWeights.at(-1) ?? "700" },
|
||||
section: {
|
||||
flexDirection: "column",
|
||||
},
|
||||
sectionHeading: {
|
||||
backgroundColor: primary,
|
||||
color: background,
|
||||
fontSize: heading.fontSize * 0.8,
|
||||
// Tracking breaks the joins of cursive right-to-left scripts such as Arabic.
|
||||
letterSpacing: r.rtl ? 0 : heading.fontSize * 0.1,
|
||||
textTransform: "uppercase",
|
||||
// Longhands: the heading's icon row strips these from its title text, but not the shorthands.
|
||||
paddingTop: metrics.gapY(0.4),
|
||||
paddingBottom: metrics.gapY(0.4),
|
||||
paddingLeft: cellX,
|
||||
paddingRight: cellX,
|
||||
},
|
||||
// Every cell is closed on all four sides, so an entry carried onto the next page still reads as a cell.
|
||||
// Forme strokes a border centred on the box's edge: touching cells share one rule, and half a rule of
|
||||
// padding keeps the first cell's top rule clear of the title bar.
|
||||
sectionItems: {
|
||||
rowGap: 0,
|
||||
paddingTop: ruleWidth / 2,
|
||||
},
|
||||
item: {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
borderWidth: ruleWidth,
|
||||
borderColor: rule,
|
||||
paddingVertical: cellY,
|
||||
paddingHorizontal: cellX,
|
||||
},
|
||||
levelContainer: {
|
||||
width: "100%",
|
||||
},
|
||||
header: {
|
||||
flexDirection: r.row,
|
||||
borderWidth: 1,
|
||||
borderColor: primary,
|
||||
},
|
||||
headerFields: {
|
||||
flex: 1,
|
||||
},
|
||||
headerIdentity: {
|
||||
...r.headerIdentity,
|
||||
rowGap: metrics.gapY(0.35),
|
||||
paddingVertical: cellY * 1.25,
|
||||
paddingHorizontal: cellX,
|
||||
},
|
||||
headerName: {
|
||||
fontSize: heading.fontSize * 2,
|
||||
lineHeight: headerNameLineHeight,
|
||||
},
|
||||
headerHeadline: {
|
||||
color: primary,
|
||||
},
|
||||
contactList: {
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
...(r.rtl ? { marginRight: -ruleWidth } : { marginLeft: -ruleWidth }),
|
||||
},
|
||||
contactItem: {
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 3),
|
||||
flexBasis: "50%",
|
||||
flexGrow: 1,
|
||||
borderTopWidth: ruleWidth,
|
||||
borderTopColor: rule,
|
||||
...ruleStart,
|
||||
paddingVertical: cellY * 0.6,
|
||||
paddingHorizontal: cellX,
|
||||
},
|
||||
pictureCell: {
|
||||
justifyContent: "center",
|
||||
...(r.rtl
|
||||
? { borderRightWidth: ruleWidth, borderRightColor: rule }
|
||||
: { borderLeftWidth: ruleWidth, borderLeftColor: rule }),
|
||||
padding: cellX,
|
||||
},
|
||||
sections: {
|
||||
flexDirection: "column",
|
||||
},
|
||||
});
|
||||
|
||||
const accentFor = ({ colors }: TemplateStyleContext) => colors.primary;
|
||||
|
||||
return {
|
||||
colors,
|
||||
styles: {
|
||||
...baseStyles,
|
||||
levelItem: (context) => ({ borderColor: accentFor(context) }),
|
||||
levelItemActive: (context) => ({ backgroundColor: accentFor(context) }),
|
||||
icon: createIconSlot({ metadata, accentFor }),
|
||||
sectionHeadingIcon: { color: background },
|
||||
} satisfies PorygonStyles,
|
||||
};
|
||||
}, [metadata, r, primary, metrics, base, foreground, background]);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
import { itemHeaderRowPart } from "../../semantic/shared-parts";
|
||||
|
||||
export const porygonSemanticManifest = {
|
||||
template: "porygon",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main", "sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: null,
|
||||
parts: [itemHeaderRowPart],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
@@ -15,7 +15,7 @@ type BaseTemplateStylesInput = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the ~20 byte-identical style slots shared by all 15 templates as plain objects.
|
||||
* Returns the ~20 byte-identical style slots shared by every template as plain objects.
|
||||
* Each template spreads the result into its own StyleSheet.create() call and overrides only
|
||||
* the slots that differ (heading fontWeight, bold fallback, inline gap, picture extras, etc.).
|
||||
*
|
||||
@@ -129,7 +129,7 @@ export function createBaseTemplateStyles({
|
||||
alignEnd: { ...r.alignEnd } satisfies Style,
|
||||
|
||||
/**
|
||||
* Standard picture style shared by 14/15 templates.
|
||||
* Standard picture style shared by every template but ditto.
|
||||
* ditto overrides: spread this and add `position: "absolute"`, `top`, `left`, `marginLeft`.
|
||||
*/
|
||||
picture: {
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import type { TemplatePageProps } from "../../document";
|
||||
import type { Style } from "../../forme/style-types";
|
||||
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
|
||||
import { useMemo } from "react";
|
||||
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 {
|
||||
CustomFieldContactItem,
|
||||
EmailContactItem,
|
||||
LocationContactItem,
|
||||
PhoneContactItem,
|
||||
WebsiteContactItem,
|
||||
} from "../shared/contact-item";
|
||||
import { TemplateProvider } from "../shared/context";
|
||||
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
|
||||
import { filterSections } from "../shared/filtering";
|
||||
import { getTemplateMetrics } from "../shared/metrics";
|
||||
import { hasTemplatePicture } from "../shared/picture";
|
||||
import {
|
||||
Heading,
|
||||
SemanticContactListView,
|
||||
SemanticHeaderPicture,
|
||||
SemanticHeaderView,
|
||||
SemanticRegionTemplatePartView,
|
||||
SemanticRegionView,
|
||||
Text,
|
||||
} from "../shared/primitives";
|
||||
import { Section } from "../shared/sections";
|
||||
import { composeStyles } from "../shared/styles";
|
||||
import { createIconSlot, useTemplateBase } from "../shared/template-base";
|
||||
|
||||
type SmeargleStyles = Omit<TemplateStyleSlots, "page"> & {
|
||||
page: Style;
|
||||
header: Style;
|
||||
headerTitle: Style;
|
||||
headerKicker: Style;
|
||||
headerName: Style;
|
||||
contactList: Style;
|
||||
contactItem: Style;
|
||||
contactText: Style;
|
||||
picture: Style;
|
||||
standfirst: Style;
|
||||
sections: Style;
|
||||
};
|
||||
|
||||
type SmeargleTemplate = {
|
||||
colors: TemplateColorRoles;
|
||||
styles: SmeargleStyles;
|
||||
/** The summary set as a standfirst: the larger, italic introduction under a magazine headline. */
|
||||
standfirstStyles: TemplateStyleSlots;
|
||||
};
|
||||
|
||||
type SmeargleHeaderProps = {
|
||||
styles: SmeargleStyles;
|
||||
};
|
||||
|
||||
/**
|
||||
* Smeargle sets the resume like the opening page of a magazine feature: the headline as a small
|
||||
* uppercase kicker over a large display name, the summary as an italic standfirst, and italic
|
||||
* section headings. Contrast between the heading and body type carries the hierarchy, not boxes.
|
||||
*/
|
||||
export const SmearglePage = ({ 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, standfirstStyles } = useSmeargleTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = page.fullWidth ? [] : filterSections(page.sidebar, data);
|
||||
const { featuredSummarySection, regularSections } = getFeaturedSummaryLayout({
|
||||
sections: mainSections,
|
||||
canFeatureSummary: showHeader,
|
||||
});
|
||||
const sections = useRenderedSectionIds(pageNodeKey, [
|
||||
...regularSections,
|
||||
...(featuredSummarySection ? sidebarSections.filter((section) => section !== "summary") : sidebarSections),
|
||||
]);
|
||||
|
||||
return (
|
||||
<Page
|
||||
{...semanticPageProps}
|
||||
size={semanticPageSize ?? pageSize}
|
||||
style={composeStyles(styles.page, pageMinHeightStyle, semanticPageStyle)}
|
||||
>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={styles} colors={colors}>
|
||||
{showHeader && <Header styles={styles} />}
|
||||
|
||||
{featuredSummarySection && (
|
||||
<SemanticRegionTemplatePartView region="featured" partKeys={["featured-summary"]} style={styles.standfirst}>
|
||||
<TemplateProvider pageNodeKey={pageNodeKey} styles={standfirstStyles} colors={colors}>
|
||||
<Section section={featuredSummarySection} placement="main" showHeading={false} />
|
||||
</TemplateProvider>
|
||||
</SemanticRegionTemplatePartView>
|
||||
)}
|
||||
|
||||
<SemanticRegionView region="main" style={composeStyles(styles.sections, { rowGap: metrics.sectionGap })}>
|
||||
{sections.map((section) => (
|
||||
<Section key={section} section={section} placement="main" />
|
||||
))}
|
||||
</SemanticRegionView>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ styles }: SmeargleHeaderProps) => {
|
||||
const { basics, picture } = useRender();
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
|
||||
return (
|
||||
<SemanticHeaderView style={styles.header}>
|
||||
<View style={styles.headerTitle}>
|
||||
{basics.headline && <Text style={styles.headerKicker}>{basics.headline}</Text>}
|
||||
<Heading style={styles.headerName}>{basics.name}</Heading>
|
||||
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} textStyle={styles.contactText} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} textStyle={styles.contactText} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} textStyle={styles.contactText} />
|
||||
<WebsiteContactItem website={basics.website} style={styles.contactItem} textStyle={styles.contactText} />
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem
|
||||
key={field.id}
|
||||
field={field}
|
||||
style={styles.contactItem}
|
||||
textStyle={styles.contactText}
|
||||
/>
|
||||
))}
|
||||
</SemanticContactListView>
|
||||
</View>
|
||||
|
||||
{hasPicture && <SemanticHeaderPicture src={picture.url} style={styles.picture} />}
|
||||
</SemanticHeaderView>
|
||||
);
|
||||
};
|
||||
|
||||
const useSmeargleTemplate = (): SmeargleTemplate => {
|
||||
const { metadata, r, foreground, background, primary, metrics, base } = useTemplateBase();
|
||||
|
||||
return useMemo(() => {
|
||||
const colors: TemplateColorRoles = { foreground, background, primary };
|
||||
const { body, heading } = metadata.typography;
|
||||
const displayWeight = heading.fontWeights[0] ?? "400";
|
||||
const smallSize = body.fontSize * 0.85;
|
||||
|
||||
const baseStyles = StyleSheet.create({
|
||||
...base,
|
||||
page: {
|
||||
...base.page,
|
||||
paddingHorizontal: metrics.page.paddingHorizontal,
|
||||
paddingVertical: metrics.page.paddingVertical,
|
||||
rowGap: metrics.sectionGap,
|
||||
},
|
||||
section: {
|
||||
flexDirection: "column",
|
||||
rowGap: metrics.gapY(0.5),
|
||||
},
|
||||
sectionHeading: {
|
||||
fontWeight: displayWeight,
|
||||
fontStyle: "italic",
|
||||
color: foreground,
|
||||
},
|
||||
item: {
|
||||
rowGap: metrics.gapY(0.125),
|
||||
},
|
||||
levelContainer: {
|
||||
width: "100%",
|
||||
},
|
||||
header: {
|
||||
flexDirection: r.row,
|
||||
alignItems: "flex-end",
|
||||
columnGap: metrics.gapX(1.5),
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: foreground,
|
||||
paddingBottom: metrics.gapY(1.25),
|
||||
},
|
||||
headerTitle: {
|
||||
flex: 1,
|
||||
...r.headerIdentity,
|
||||
rowGap: metrics.gapY(0.6),
|
||||
},
|
||||
headerKicker: {
|
||||
fontSize: smallSize,
|
||||
fontWeight: base.bold.fontWeight,
|
||||
// Tracking breaks the joins of cursive right-to-left scripts such as Arabic.
|
||||
letterSpacing: r.rtl ? 0 : smallSize * 0.18,
|
||||
textTransform: "uppercase",
|
||||
color: primary,
|
||||
},
|
||||
headerName: {
|
||||
fontSize: heading.fontSize * 2.25,
|
||||
fontWeight: displayWeight,
|
||||
lineHeight: 1.05,
|
||||
letterSpacing: -heading.fontSize * 0.02,
|
||||
},
|
||||
contactList: {
|
||||
flexDirection: r.row,
|
||||
flexWrap: "wrap",
|
||||
rowGap: metrics.gapY(0.25),
|
||||
columnGap: metrics.gapX(1),
|
||||
marginTop: metrics.gapY(0.25),
|
||||
},
|
||||
contactItem: {
|
||||
flexDirection: r.row,
|
||||
alignItems: "center",
|
||||
columnGap: metrics.gapX(1 / 6),
|
||||
},
|
||||
contactText: {
|
||||
fontSize: smallSize,
|
||||
},
|
||||
standfirst: {
|
||||
width: "85%",
|
||||
alignSelf: r.rtl ? "flex-end" : "flex-start",
|
||||
},
|
||||
sections: {
|
||||
flexDirection: "column",
|
||||
},
|
||||
});
|
||||
|
||||
// The standfirst keeps the body face, set larger and in italic.
|
||||
const standfirstText = {
|
||||
fontSize: body.fontSize * 1.15,
|
||||
fontStyle: "italic",
|
||||
lineHeight: 1.45,
|
||||
} satisfies Style;
|
||||
|
||||
const accentFor = ({ colors }: TemplateStyleContext) => colors.primary;
|
||||
|
||||
const styles = {
|
||||
...baseStyles,
|
||||
levelItem: (context) => ({ borderColor: accentFor(context) }),
|
||||
levelItemActive: (context) => ({ backgroundColor: accentFor(context) }),
|
||||
icon: createIconSlot({ metadata, accentFor }),
|
||||
} satisfies SmeargleStyles;
|
||||
|
||||
return {
|
||||
colors,
|
||||
styles,
|
||||
standfirstStyles: {
|
||||
...styles,
|
||||
richParagraph: { ...baseStyles.richParagraph, ...standfirstText },
|
||||
richListItemMarker: { ...baseStyles.richListItemMarker, ...standfirstText },
|
||||
richListItemContent: { ...baseStyles.richListItemContent, ...standfirstText },
|
||||
},
|
||||
};
|
||||
}, [metadata, r, primary, metrics, base, foreground, background]);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { TemplateSemanticManifest } from "../../semantic/template-manifest";
|
||||
import { itemHeaderRowPart } from "../../semantic/shared-parts";
|
||||
|
||||
export const smeargleSemanticManifest = {
|
||||
template: "smeargle",
|
||||
regions: [
|
||||
{ name: "header", placement: "main", origins: [] },
|
||||
{ name: "featured", placement: "main", origins: [] },
|
||||
{ name: "main", placement: "main", origins: ["main", "sidebar"] },
|
||||
],
|
||||
header: { region: "header", placement: "main" },
|
||||
specialSummary: { region: "featured", placement: "main", source: "main-with-header" },
|
||||
parts: [
|
||||
itemHeaderRowPart,
|
||||
{
|
||||
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" },
|
||||
},
|
||||
],
|
||||
} as const satisfies TemplateSemanticManifest;
|
||||
Reference in New Issue
Block a user