@sevenfold/setto-client 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ import { type BuiltinSectionProps } from '../pages/SettoPage';
2
+ export declare function SettoBlogSection({ sectionId, first }: BuiltinSectionProps): import("react/jsx-runtime").JSX.Element;
3
+ /**
4
+ * The blog for a site that routes by hand instead of through `pages.json`
5
+ * (setto.no): mount it on `<basePath>` and `<basePath>/*`, and pass the same
6
+ * path to `settoBlog({ blogPath })`.
7
+ *
8
+ * <Route path="/blogg/*" element={<SettoBlog basePath="/blogg" />} />
9
+ */
10
+ export declare function SettoBlog({ basePath, title }: {
11
+ basePath?: string;
12
+ title?: string;
13
+ }): import("react/jsx-runtime").JSX.Element;
14
+ export declare function SettoBlogLatest({ sectionId, first }: BuiltinSectionProps): import("react/jsx-runtime").JSX.Element | null;
15
+ /**
16
+ * Shape is scoped `.setto-blog :where(…)` (one class: beats Preflight's
17
+ * element resets); colour stays at zero specificity so the site's own wins.
18
+ * Same split, same reason, as the form's sheet.
19
+ */
20
+ export declare const BLOG_CSS = "\n.setto-blog{padding:clamp(48px,8vw,96px) 16px}\n.setto-blog :where(.setto-blog__inner){max-width:68ch;margin:0 auto}\n.setto-blog :where(.setto-blog__inner--wide){max-width:1120px}\n.setto-blog :where(.setto-blog__title){font-family:var(--setto-display-font,inherit);font-size:clamp(2rem,4vw,2.75rem);line-height:1.15;font-weight:600;margin:0 0 .75em}\n.setto-blog :where(.setto-blog__list){display:grid;gap:clamp(1.5rem,3vw,2.5rem);grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr))}\n.setto-blog :where(.setto-blog__card){display:grid;gap:.9rem;align-content:start}\n.setto-blog :where(.setto-blog__card-cover){display:block;overflow:hidden;border-radius:.9rem;aspect-ratio:16/10}\n.setto-blog :where(.setto-blog__card-cover img,.setto-blog__cover img,.setto-blog__figure img,.setto-blog__gallery-item img){display:block;width:100%;height:100%;object-fit:cover}\n.setto-blog :where(.setto-blog__card-body){display:grid;gap:.45rem}\n.setto-blog :where(.setto-blog__card-title){font-family:var(--setto-display-font,inherit);font-size:1.3rem;line-height:1.25;font-weight:600;margin:0}\n.setto-blog :where(.setto-blog__card-lead){margin:0;line-height:1.55}\n.setto-blog :where(.setto-blog__date){font-size:.875rem;letter-spacing:.01em}\n.setto-blog :where(.setto-blog__more,.setto-blog__back){display:inline-flex;gap:.35em;align-items:center;font-weight:500;width:fit-content}\n.setto-blog :where(.setto-blog__back){margin-bottom:2rem}\n.setto-blog :where(.setto-blog__latest-head){display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.5rem 1.5rem;margin-bottom:1.75rem}\n.setto-blog :where(.setto-blog__latest-head .setto-blog__title){margin:0}\n.setto-blog :where(.setto-blog__post-header){display:grid;gap:.6rem;margin-bottom:2rem}\n.setto-blog :where(.setto-blog__post-title){font-family:var(--setto-display-font,inherit);font-size:clamp(2rem,5vw,3rem);line-height:1.1;font-weight:600;margin:0}\n.setto-blog :where(.setto-blog__lead){font-size:1.2rem;line-height:1.5;margin:0}\n.setto-blog :where(.setto-blog__cover){margin:0 0 2.5rem;overflow:hidden;border-radius:1rem;aspect-ratio:16/9}\n.setto-blog :where(.setto-blog__prose){font-size:1.075rem;line-height:1.7}\n.setto-blog :where(.setto-blog__prose > * + *){margin-top:1.15em}\n.setto-blog :where(.setto-blog__prose h2,.setto-blog__prose h3,.setto-blog__prose h4){font-family:var(--setto-display-font,inherit);line-height:1.25;font-weight:600;margin:1.8em 0 0}\n.setto-blog :where(.setto-blog__prose h2){font-size:1.6rem}\n.setto-blog :where(.setto-blog__prose h3){font-size:1.3rem}\n.setto-blog :where(.setto-blog__prose h4){font-size:1.1rem}\n.setto-blog :where(.setto-blog__prose ul,.setto-blog__prose ol){padding-left:1.4em}\n.setto-blog :where(.setto-blog__prose ul){list-style:disc}\n.setto-blog :where(.setto-blog__prose ol){list-style:decimal}\n.setto-blog :where(.setto-blog__prose li + li){margin-top:.35em}\n.setto-blog :where(.setto-blog__prose blockquote){margin-left:0;padding-left:1.1em;border-left-width:3px;border-left-style:solid;font-style:italic}\n.setto-blog :where(.setto-blog__prose pre){overflow-x:auto;padding:1em;border-radius:.6rem;font-size:.9em}\n.setto-blog :where(.setto-blog__prose code){font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em}\n.setto-blog :where(.setto-blog__prose hr){border:0;border-top-width:1px;border-top-style:solid;margin:2.5em 0}\n.setto-blog :where(.setto-blog__prose a){text-decoration:underline;text-underline-offset:.18em}\n.setto-blog :where(.setto-blog__table){overflow-x:auto}\n.setto-blog :where(.setto-blog__table table){border-collapse:collapse;width:100%;font-size:.95em}\n.setto-blog :where(.setto-blog__table th,.setto-blog__table td){padding:.55em .8em;border-bottom-width:1px;border-bottom-style:solid;text-align:left}\n.setto-blog :where(.setto-blog__figure,.setto-blog__gallery,.setto-blog__video){margin:2em 0}\n.setto-blog :where(.setto-blog__figure img){border-radius:.8rem;height:auto}\n.setto-blog :where(figcaption){font-size:.9rem;margin-top:.6em;opacity:.75}\n.setto-blog :where(.setto-blog__gallery-grid){display:grid;gap:.6rem;grid-template-columns:repeat(auto-fit,minmax(min(100%,150px),1fr))}\n.setto-blog :where(.setto-blog__gallery-item){margin:0}\n.setto-blog :where(.setto-blog__gallery-item img){border-radius:.6rem;aspect-ratio:4/3}\n.setto-blog :where(.setto-blog__video-frame){position:relative;aspect-ratio:16/9;border-radius:.8rem;overflow:hidden}\n.setto-blog :where(.setto-blog__video-frame iframe){position:absolute;inset:0;width:100%;height:100%;border:0}\n.setto-blog :where(.setto-blog__video-play){position:absolute;inset:0;width:100%;height:100%;padding:0;border:0;cursor:pointer;display:grid;place-items:center}\n.setto-blog :where(.setto-blog__video-play img){position:absolute;inset:0;width:100%;height:100%;object-fit:cover}\n.setto-blog :where(.setto-blog__video-icon){position:relative;display:block;line-height:0}\n.setto-blog :where(.setto-blog__callout){padding:1.1em 1.3em;border-radius:.8rem;border-left-width:4px;border-left-style:solid}\n.setto-blog :where(.setto-blog__callout > * + *){margin-top:.7em}\n.setto-blog :where(.setto-blog__callout ul,.setto-blog__callout ol){padding-left:1.3em;list-style:disc}\n.setto-blog :where(.setto-blog__share){display:flex;flex-wrap:wrap;gap:.5rem .9rem;align-items:center;margin-top:3rem;padding-top:1.5rem;border-top-width:1px;border-top-style:solid}\n.setto-blog :where(.setto-blog__share-label){font-weight:600}\n.setto-blog :where(.setto-blog__share a,.setto-blog__share button){font:inherit;padding:.35rem .85rem;border-radius:999px;border-width:1px;border-style:solid;cursor:pointer;text-decoration:none;line-height:1.4}\n.setto-blog :where(.setto-blog__empty,.setto-blog__loading){opacity:.75}\n:where(.setto-blog){color:inherit}\n:where(.setto-blog__date){opacity:.7}\n:where(.setto-blog a){color:inherit}\n:where(.setto-blog__prose blockquote,.setto-blog__prose hr,.setto-blog__table th,.setto-blog__table td,.setto-blog__share){border-color:color-mix(in srgb,currentColor 18%,transparent)}\n:where(.setto-blog__prose pre,.setto-blog__card-cover){background:color-mix(in srgb,currentColor 6%,transparent)}\n:where(.setto-blog__share a,.setto-blog__share button){border-color:color-mix(in srgb,currentColor 30%,transparent);background:transparent;color:inherit}\n:where(.setto-blog__callout){background:color-mix(in srgb,currentColor 5%,transparent);border-left-color:color-mix(in srgb,currentColor 40%,transparent)}\n:where(.setto-blog__callout[data-tone=\"tip\"]){border-left-color:#0f7b4f}\n:where(.setto-blog__callout[data-tone=\"warning\"]){border-left-color:#b54708}\n:where(.setto-blog__video-frame){background:#000}\n";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The posts a site's build hands to the library, and how a page chooses among
3
+ * them. No parser and no React: the build (`@setto/client/vite`) has already
4
+ * validated every file against the contract and turned its body into a syntax
5
+ * tree, so the browser only walks data.
6
+ */
7
+ import type { Root } from 'mdast';
8
+ import type { BlogFrontmatter } from './contract';
9
+ /** A post as the list knows it: its frontmatter, and nothing to download. */
10
+ export type BlogPostSummary = BlogFrontmatter;
11
+ /** A post as its page knows it: frontmatter and the body's syntax tree. */
12
+ export interface BlogPostData {
13
+ frontmatter: BlogFrontmatter;
14
+ body: Root;
15
+ }
16
+ /**
17
+ * What `virtual:setto-blog` exports and `SettoConfig.blog` carries. Bodies are
18
+ * loaded one at a time, so a site with a hundred posts does not ship a
19
+ * hundred articles to read one.
20
+ */
21
+ export interface SettoBlogData {
22
+ posts: readonly BlogPostSummary[];
23
+ load(slug: string, locale: string): Promise<BlogPostData | null>;
24
+ }
25
+ /** Newest first; a tie breaks on slug so the order never flickers. */
26
+ export declare function byDateDesc(a: BlogPostSummary, b: BlogPostSummary): number;
27
+ /**
28
+ * One entry per post, in the reader's language where it exists and in the
29
+ * site's own otherwise. A post written only in Norwegian still shows to an
30
+ * English reader — untranslated is better than missing.
31
+ */
32
+ export declare function postsForLanguage(posts: readonly BlogPostSummary[], language: string, fallbackLanguage: string): BlogPostSummary[];
33
+ /** The version of one post to show this reader, or null if there is none. */
34
+ export declare function postForSlug(posts: readonly BlogPostSummary[], slug: string, language: string, fallbackLanguage: string): BlogPostSummary | null;
35
+ /** `2026-10-01` as «1. oktober 2026» or «1 October 2026». */
36
+ export declare function formatPostDate(date: string, language: string): string;
@@ -0,0 +1,16 @@
1
+ import type { RootContent } from 'mdast';
2
+ export declare function BlogBody({ nodes, language }: {
3
+ nodes: readonly RootContent[];
4
+ language: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ /**
7
+ * An image, resized by the site's optimizer when it has one and the image is
8
+ * one it may read (`isOptimizable`: this site's own, or a blog photograph in
9
+ * Setto's media store). If a variant fails, the original is shown instead.
10
+ */
11
+ export declare function BlogImg({ src, alt, sizes, className }: {
12
+ src: string;
13
+ alt: string;
14
+ sizes: string;
15
+ className?: string;
16
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The few words the blog says itself — around the posts, never in them. In
3
+ * Norwegian and English; any other language reads English, which is what a
4
+ * visitor who chose it can most likely read.
5
+ */
6
+ declare const NO: {
7
+ blog: string;
8
+ latest: string;
9
+ allPosts: string;
10
+ readMore: string;
11
+ back: string;
12
+ empty: string;
13
+ notFound: string;
14
+ loading: string;
15
+ share: string;
16
+ moreShare: string;
17
+ copyLink: string;
18
+ copied: string;
19
+ email: string;
20
+ video: string;
21
+ play: string;
22
+ };
23
+ export type BlogStrings = typeof NO;
24
+ export declare function blogStrings(language: string): BlogStrings;
25
+ export {};
package/dist/index.d.ts CHANGED
@@ -14,6 +14,9 @@ export { SettoImage } from './SettoImage';
14
14
  export type { SettoImageProps, ImageToolsProps } from './SettoImage';
15
15
  export { SETTO_IMAGE_QUALITY, SETTO_IMAGE_WIDTHS, isOptimizable, optimizedSrcSet, optimizedUrl, settoVercelImagesConfig, } from './lib/optimized-image';
16
16
  export { SETTO_MEDIA_HOST, isSettoMediaUrl } from './lib/media';
17
+ export { SettoBlog, SettoBlogSection, SettoBlogLatest, BLOG_CSS } from './blog/SettoBlog';
18
+ export { postsForLanguage, postForSlug, formatPostDate } from './blog/data';
19
+ export type { SettoBlogData, BlogPostData, BlogPostSummary } from './blog/data';
17
20
  export { SettoRepeater } from './SettoRepeater';
18
21
  export type { SettoRepeaterProps } from './SettoRepeater';
19
22
  export { SettoForm, FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS, fieldTypeHasOptions, fieldTypeHasPlaceholder, formFieldsPrefix, isFormFieldType, readLocaleFields, settoSubmissionUrl, } from './modules/forms';
@@ -28,7 +31,7 @@ export type { SettoMapProps } from './SettoMap';
28
31
  export type { MapLocation, MapToolsProps, MapToolsComponent } from './map-tools';
29
32
  export { SettoPage, SettoTextSection, usePageManifest, useSettoMenu, } from './pages/SettoPage';
30
33
  export type { SettoPageProps, BuiltinSectionProps } from './pages/SettoPage';
31
- export { PAGES_PATH, PAGES_MANIFEST_VERSION, BUILTIN_SECTION_TYPES, parseManifest, normalisePath, pageForPath, pageById, findSection, visibleMenu, applyOp, applyOps, builtinKeys, pageTitleKey, serialiseManifest, isSafeHref, ManifestError, } from './pages/manifest';
34
+ export { PAGES_PATH, PAGES_MANIFEST_VERSION, BUILTIN_SECTION_TYPES, parseManifest, normalisePath, pageForPath, blogPage, blogSlugForPath, pageById, findSection, visibleMenu, applyOp, applyOps, builtinKeys, pageTitleKey, serialiseManifest, isSafeHref, ManifestError, } from './pages/manifest';
32
35
  export type { PagesManifest, PageEntry, PageSection, MenuItem, MenuList, ManifestOp, ManifestIssue, ManifestParse, BuiltinSectionType, } from './pages/manifest';
33
36
  export { PageStore, EMPTY_PAGE_STORE_SNAPSHOT, countChanges } from './lib/page-store';
34
37
  export type { PageStoreSnapshot } from './lib/page-store';
@@ -26,8 +26,14 @@ export declare const PAGES_PATH = "src/content/pages.json";
26
26
  * `text` is a heading, an optional intro and a list of paragraphs with
27
27
  * optional headings: enough for terms, privacy, an «about» page. It is what
28
28
  * lets Pixel add a «Vilkår» page in the chat instead of in a sandbox.
29
+ *
30
+ * `blog` is the blog (R24): the list of posts on its page, and a single post
31
+ * at `<page path>/<slug>` — the one page whose path has room below it. At
32
+ * most one per site, and never on the front page, which would swallow every
33
+ * address. `blog-latest` is a band of the newest posts, for the front page.
34
+ * Both read the posts the site's build bundles (`SettoConfig.blog`).
29
35
  */
30
- export declare const BUILTIN_SECTION_TYPES: readonly ["text"];
36
+ export declare const BUILTIN_SECTION_TYPES: readonly ["text", "blog", "blog-latest"];
31
37
  export type BuiltinSectionType = (typeof BUILTIN_SECTION_TYPES)[number];
32
38
  export interface PageSection {
33
39
  /** Unique across the whole site: it is also the theme id and the anchor. */
@@ -85,6 +91,17 @@ export declare function isSafeHref(href: string): boolean;
85
91
  /** `/vilkar/` and `/vilkar` are one page; the empty path is the front page. */
86
92
  export declare function normalisePath(path: string): string;
87
93
  export declare function pageForPath(manifest: PagesManifest, path: string): PageEntry | null;
94
+ /** The page holding the site's blog, and the section on it. */
95
+ export declare function blogPage(manifest: PagesManifest): {
96
+ page: PageEntry;
97
+ section: PageSection;
98
+ } | null;
99
+ /**
100
+ * The post a path names: `/blogg/ny-kaffemaskin` → `ny-kaffemaskin` when the
101
+ * blog lives at `/blogg`. One segment below the blog page and a valid slug;
102
+ * anything else is not a post.
103
+ */
104
+ export declare function blogSlugForPath(manifest: PagesManifest, path: string): string | null;
88
105
  export declare function pageById(manifest: PagesManifest, id: string): PageEntry | null;
89
106
  /** The page a section is on, and where. */
90
107
  export declare function findSection(manifest: PagesManifest, sectionId: string): {