Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/self-hosted/hosting/api/src/style-template-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ export const STYLE_TEMPLATE_DISPLAY = {
},
headingStyle: 'sans',
},
journal: {
name: 'Journal',
tagline: 'Ink on paper: one quiet column for long-form writing',
colors: {
background: '#faf8f4',
surface: '#f2efe8',
accent: '#9c4a1e',
text: '#221d17',
},
headingStyle: 'serif',
},
} satisfies Record<StyleTemplate, StyleTemplateDisplay>;

export function templateCatalog() {
Expand Down
1 change: 1 addition & 0 deletions apps/self-hosted/hosting/api/src/style-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const STYLE_TEMPLATES = Object.freeze([
'magazine',
'developer',
'modern-gradient',
'journal',
] as const);

export type StyleTemplate = (typeof STYLE_TEMPLATES)[number];
Expand Down
3 changes: 3 additions & 0 deletions apps/self-hosted/src/core/i18n-strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export type TranslationKey =
| 'panel_configuration_general_style_template_magazine_option'
| 'panel_configuration_general_style_template_developer_option'
| 'panel_configuration_general_style_template_modern_gradient_option'
| 'panel_configuration_general_style_template_journal_option'
| 'panel_configuration_general_language_label'
| 'panel_configuration_general_language_description'
| 'panel_configuration_general_language_en_option'
Expand Down Expand Up @@ -590,6 +591,8 @@ export const translations: { en: Translations } & Record<
panel_configuration_general_style_template_magazine_option: 'Magazine (Editorial)',
panel_configuration_general_style_template_developer_option: 'Developer (Tech)',
panel_configuration_general_style_template_modern_gradient_option: 'Modern Gradient',
panel_configuration_general_style_template_journal_option:
'Journal (single column, serif, no sidebar)',
panel_configuration_general_language_label: 'Language',
panel_configuration_general_language_description: 'Default language',
panel_configuration_general_language_en_option: 'English',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { t } from '@/core';
import { useInstanceConfig } from '../hooks/use-instance-config';
import { BlogPostItem } from './blog-post-item';
import { useThemeComponents } from '@/themes/use-theme-components';

interface Props {
query: string;
Expand Down Expand Up @@ -35,6 +35,9 @@ function searchResultToEntry(result: SearchResult): Entry {
}

export function SearchResults({ query }: Props) {
// The entry card resolves through the theme registry, so search results
// wear the active theme's entry look instead of the default card.
const { PostCard } = useThemeComponents();
const { username, communityId, isCommunityMode } = useInstanceConfig();

// Build search query scoped to the blog/community
Expand Down Expand Up @@ -92,7 +95,7 @@ export function SearchResults({ query }: Props) {
</div>
<div className="blog-posts-list">
{results.map((result, index) => (
<BlogPostItem
<PostCard
key={`${result.author}/${result.permlink}`}
entry={searchResultToEntry(result)}
index={index}
Expand Down
29 changes: 29 additions & 0 deletions apps/self-hosted/src/features/floating-menu/config-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,3 +863,32 @@ describe('isFieldVisible (the visibleWhen capability)', () => {
expect(isFieldVisible(broken, doc)).toBe(true);
});
});

describe('theme-gated layout options', () => {
const fields = buildConfigFields((key) => key);
const layout = (fields.configuration as any).fields.instanceConfiguration.fields.layout.fields;
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated

function docWithTemplate(styleTemplate?: string) {
return {
configuration: { general: styleTemplate ? { styleTemplate } : {} },
} as unknown as Record<string, import('./types').ConfigValue>;
}

it('hides the sidebar section and list type under a theme that declares them unsupported', () => {
expect(isFieldVisible(layout.sidebar, docWithTemplate('journal'))).toBe(false);
expect(isFieldVisible(layout.listType, docWithTemplate('journal'))).toBe(false);
});

it('shows them for every CSS-only template and for an unset template', () => {
for (const template of ['medium', 'minimal', 'magazine', 'developer', 'modern-gradient']) {
expect(isFieldVisible(layout.sidebar, docWithTemplate(template))).toBe(true);
}
expect(isFieldVisible(layout.listType, docWithTemplate())).toBe(true);
});

it('follows the UNSAVED draft, so switching templates in the panel reacts immediately', () => {
// The predicate reads the edited document, not the applied config.
expect(isFieldVisible(layout.sidebar, docWithTemplate('journal'))).toBe(false);
expect(isFieldVisible(layout.sidebar, docWithTemplate('medium'))).toBe(true);
});
});
24 changes: 24 additions & 0 deletions apps/self-hosted/src/features/floating-menu/config-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ import {
STYLE_TEMPLATES,
type StyleTemplate,
} from '../../../hosting/api/src/style-templates';
import { isThemeOptionSupported } from '@/themes/registry';

/**
* The configured style template out of the EDITED document, for visibleWhen
* predicates: visibility must follow the unsaved draft, so switching the
* template in the panel immediately shows or hides the options that theme
* consumes.
*/
function editedStyleTemplate(document: Record<string, ConfigValue>): unknown {
const configuration = document?.configuration as
| { general?: { styleTemplate?: unknown } }
| undefined;
return configuration?.general?.styleTemplate;
}

/**
* One label key per roster entry, `satisfies` so adding a template to the
Expand All @@ -31,6 +45,7 @@ const STYLE_TEMPLATE_LABEL_KEYS = {
developer: 'panel_configuration_general_style_template_developer_option',
'modern-gradient':
'panel_configuration_general_style_template_modern_gradient_option',
journal: 'panel_configuration_general_style_template_journal_option',
} satisfies Record<StyleTemplate, TranslationKey>;

export type ConfigFieldType =
Expand Down Expand Up @@ -240,6 +255,11 @@ export function buildConfigFields(
listType: {
label: t('panel_configuration_instance_configuration_layout_list_type_label'),
type: 'select',
// Declared unsupported by themes whose entry component is not
// list/grid switchable (Journal renders one column of plain
// entries). Hidden, not inert: the stored value is untouched.
visibleWhen: (document) =>
isThemeOptionSupported(editedStyleTemplate(document), 'listType'),
description: t('panel_configuration_instance_configuration_layout_list_type_description'),
options: [
{ value: 'list', label: t('panel_configuration_instance_configuration_layout_list_type_list_option') },
Expand All @@ -260,6 +280,10 @@ export function buildConfigFields(
sidebar: {
label: t('panel_configuration_instance_configuration_layout_sidebar_label'),
type: 'section',
// Themes whose shell renders no sidebar declare it unsupported;
// the whole section hides rather than sitting there doing nothing.
visibleWhen: (document) =>
isThemeOptionSupported(editedStyleTemplate(document), 'sidebar'),
fields: {
placement: {
label: t('panel_configuration_instance_configuration_layout_sidebar_placement_label'),
Expand Down
8 changes: 4 additions & 4 deletions apps/self-hosted/src/styles/theme-appearance-tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,10 @@ const accentBlocks = ALL_BLOCKS.filter((block) =>

describe('accent blocks', () => {
it('are every light and dark palette the templates declare', () => {
// Five templates in two modes, plus the two base blocks in variables.css.
// Six templates in two modes, plus the two base blocks in variables.css.
// A parser that stopped seeing them would make every check below vacuous.
expect(accentBlocks.length).toBe(12);
expect(new Set(accentBlocks.map((block) => block.file)).size).toBe(6);
expect(accentBlocks.length).toBe(14);
expect(new Set(accentBlocks.map((block) => block.file)).size).toBe(7);
});

it('declare the chip text next to the accent that fills the chip', () => {
Expand Down Expand Up @@ -326,7 +326,7 @@ describe('card treatment', () => {
);

it('is stated by every template rather than inherited by accident', () => {
expect(templates.length).toBe(5);
expect(templates.length).toBe(6);
const missing: string[] = [];
for (const block of templates) {
for (const token of CARD_TOKENS) {
Expand Down
1 change: 1 addition & 0 deletions apps/self-hosted/src/styles/themes/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
@import "./magazine.css";
@import "./developer.css";
@import "./modern-gradient.css";
@import "./journal.css";
114 changes: 114 additions & 0 deletions apps/self-hosted/src/styles/themes/journal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* Journal Theme - Ink on paper
*
* The first layout-level design: a single centered column, no cards, no
* sidebar, an author block up top. Serif throughout at a larger scale with
* generous leading; entries are date, title and excerpt separated by hairline
* rules. The structural half lives in src/themes/journal/ (Shell and PostCard
* manifest overrides); these tokens carry the full --theme-* contract so the
* accent correction sweep and every token utility keep working.
*/

[data-style-template="journal"] {
/* Typography */
--theme-font-body: "Source Serif 4", "Georgia", "Times New Roman", serif;
--theme-font-heading: "Source Serif 4", "Georgia", "Times New Roman", serif;
--theme-font-ui:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial,
sans-serif;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/* Typography Sizes */
--theme-text-base: 19px;
--theme-leading-normal: 1.65;
--theme-tracking-normal: 0;
--theme-tracking-tight: -0.01em;

/* Colors - Light: warm paper, near-black ink, burnt sienna accent */
--theme-bg-primary: #faf8f4;
--theme-bg-secondary: #f2efe8;
--theme-bg-tertiary: rgba(34, 29, 23, 0.05);
--theme-bg-card: #faf8f4;

--theme-text-primary: #221d17;
--theme-text-secondary: rgba(34, 29, 23, 0.75);
--theme-text-muted: rgba(34, 29, 23, 0.55);
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated

--theme-accent: #9c4a1e;
--theme-accent-hover: #7f3a15;
/* Ink on the accent fill: white on #9c4a1e is 5.4:1. */
--theme-accent-contrast: #ffffff;
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

--theme-border: rgba(34, 29, 23, 0.12);
--theme-border-strong: rgba(34, 29, 23, 0.2);

/* Effects: paper has no elevation */
--theme-radius-sm: 2px;
--theme-radius: 2px;
--theme-radius-lg: 4px;
--theme-radius-full: 9999px;

--theme-shadow-sm: none;
--theme-shadow: none;
--theme-shadow-lg: none;

/* Link underline style, derived from the accent; see variables.css */
--theme-link-decoration: underline;
--theme-link-decoration-color: color-mix(
in srgb,
var(--theme-accent-text, var(--theme-accent)) 40%,
transparent
);
--theme-link-decoration-hover: var(--theme-accent-text, var(--theme-accent));

/* Tag style: quiet, set in the UI face like marginalia */
--theme-tag-text: var(--theme-accent-text-light, rgba(34, 29, 23, 0.65));
--theme-tag-radius: 2px;

/* Card treatment: none. An entry is the page surface with a hairline rule. */
--theme-card-bg: var(--theme-bg-card);
--theme-card-border: 1px solid var(--theme-border);
--theme-card-backdrop: none;

/* Layout: one measure-width column; the sidebar tokens keep the contract
* satisfied but the Journal shell renders no sidebar at all. */
--theme-content-width: 680px;
--theme-sidebar-width: 280px;
--theme-layout-gap: 2.5rem;
--theme-layout-container-padding: 1.25rem;
--theme-layout-section-gap: 2.5rem;
--theme-card-padding: 0px;
--theme-grid-gap: 2.5rem;
--theme-grid-columns-tablet: 1;
--theme-grid-columns-desktop: 1;
--theme-post-card-image-height: 200px;
--theme-post-card-image-radius: 2px;
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}

/* Journal Dark Mode: warm charcoal, never blue-black */
[data-style-template="journal"][data-theme="dark"] {
--theme-bg-primary: #17140f;
--theme-bg-secondary: #1f1b15;
--theme-bg-tertiary: rgba(247, 242, 234, 0.07);
--theme-bg-card: #17140f;

--theme-text-primary: rgba(247, 242, 234, 0.92);
--theme-text-secondary: rgba(247, 242, 234, 0.75);
--theme-text-muted: rgba(247, 242, 234, 0.58);

--theme-accent: #d78d5c;
--theme-accent-hover: #e5a377;
/* Ink on the accent fill: the same choice the correction module makes for
* this fill, so static CSS and runtime preview agree. 7.2:1. */
--theme-accent-contrast: #111827;

--theme-border: rgba(247, 242, 234, 0.12);
--theme-border-strong: rgba(247, 242, 234, 0.2);

--theme-link-decoration-color: color-mix(
in srgb,
var(--theme-accent-text, var(--theme-accent)) 40%,
transparent
);
--theme-link-decoration-hover: var(--theme-accent-text, var(--theme-accent));

--theme-tag-text: var(--theme-accent-text-dark, rgba(247, 242, 234, 0.65));
}
58 changes: 58 additions & 0 deletions apps/self-hosted/src/themes/journal/journal-post-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Link } from '@tanstack/react-router';
import type { Entry } from '@ecency/sdk';
import { postBodySummary } from '@ecency/render-helper';
import { useMemo } from 'react';
import { formatDate } from '@/core';

interface Props {
entry: Entry;
index?: number;
}

/**
* A Journal entry: date, large serif title, excerpt, hairline rule. No card
* chrome, no image, no counters; the entry is the page surface, the way a
* personal publication reads. Stats and actions live on the post page.
*/
export function JournalPostCard({ entry }: Props) {
const entryData = entry.original_entry || entry;

const summary = useMemo(
() =>
entryData.json_metadata?.description ||
postBodySummary(entryData.body, 220),
[entryData],
);

// Same canonical link shape the default card uses: the '@' is part of the
// post URL and the router leaves it unencoded.
const postParams = useMemo(
() => ({ author: `@${entryData.author}`, permlink: entryData.permlink }),
[entryData.author, entryData.permlink],
);
const postSearch = { raw: undefined };

return (
<article className="py-8 border-b border-theme last:border-b-0">
<p className="text-sm text-theme-muted font-theme-ui mb-2">
<time dateTime={entryData.created}>{formatDate(entryData.created)}</time>
{entryData.community && entryData.community_title && (
<span> · {entryData.community_title}</span>
)}
</p>
<h2 className="heading-theme text-2xl sm:text-3xl leading-[1.2] mb-3">
<Link
to="/$author/$permlink"
params={postParams}
search={postSearch}
className="no-underline text-theme-primary hover:opacity-80 transition-theme"
>
{entryData.title}
</Link>
</h2>
{summary && (
<p className="text-theme-secondary leading-relaxed">{summary}</p>
)}
</article>
);
}
Loading
Loading