···7171- **React contexts**: `DocumentProvider`, `LeafletContentProvider` for page-level data
7272- **Inngest functions**: Async jobs in `app/api/inngest/functions/`
7373- **Icons**: Icon components live in `components/Icons/`. Each icon is a named export in its own file (e.g. `RefreshSmall.tsx`), imports `Props` from `./Props`, spreads `{...props}` on the `<svg>` element, and uses `fill="currentColor"` instead of hardcoded colors like `fill="black"`.
7474+- **Popovers and menus**: Use the existing `Popover` (`components/Popover`), `Menu`, and `MenuItem` (`components/Menu`) components — do not create new popover/menu primitives
-1
actions/createNewLeaflet.ts
···1212 email_auth_tokens,
1313} from "drizzle/schema";
1414import { redirect } from "next/navigation";
1515-import postgres from "postgres";
1615import { v7 } from "uuid";
1716import { sql, eq, and } from "drizzle-orm";
1817import { cookies } from "next/headers";
+1-1
actions/emailAuth.ts
···2233import { randomBytes } from "crypto";
44import { drizzle } from "drizzle-orm/node-postgres";
55-import postgres from "postgres";
55+66import { email_auth_tokens, identities } from "drizzle/schema";
77import { and, eq } from "drizzle-orm";
88import { cookies } from "next/headers";
+30-2
actions/getIdentityData.ts
···44import { supabaseServerClient } from "supabase/serverClient";
55import { cache } from "react";
66import { deduplicateByUri } from "src/utils/deduplicateRecords";
77+import { AtUri } from "@atproto/syntax";
88+import { TID } from "@atproto/common";
79export const getIdentityData = cache(uncachedGetIdentityData);
810export async function uncachedGetIdentityData() {
911 let cookieStore = await cookies();
···7577 .from("publications")
7678 .select("*")
7779 .eq("identity_did", auth_res.data.identities.atp_did);
7878- // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces
7979- const publications = deduplicateByUri(rawPublications || []);
8080+ // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces,
8181+ // then filter to only publications created by Leaflet
8282+ const publications = deduplicateByUri(rawPublications || []).filter(
8383+ isLeafletPublication,
8484+ );
8085 return {
8186 ...auth_res.data.identities,
8287 publications,
···9297 subscription,
9398 };
9499}
100100+101101+function isLeafletPublication(p: { uri: string; record: unknown }): boolean {
102102+ try {
103103+ const rkey = new AtUri(p.uri).rkey;
104104+ if (!TID.is(rkey)) return false;
105105+ } catch {
106106+ return false;
107107+ }
108108+109109+ const record = p.record as Record<string, any> | null;
110110+ if (!record) return true;
111111+112112+ if (record.preferences?.greengale) return false;
113113+114114+ if (
115115+ record.theme &&
116116+ record.theme.$type &&
117117+ record.theme.$type !== "pub.leaflet.publication#theme"
118118+ )
119119+ return false;
120120+121121+ return true;
122122+}
···2233import { drizzle } from "drizzle-orm/node-postgres";
44import { permission_token_on_homepage } from "drizzle/schema";
55-import postgres from "postgres";
55+66import { v7 } from "uuid";
77import { sql, eq, inArray, and } from "drizzle-orm";
88import { cookies } from "next/headers";
-71
actions/sendUpdateToRSVPS.ts
···11-"use server";
22-import { drizzle } from "drizzle-orm/node-postgres";
33-import { eq } from "drizzle-orm";
44-import {
55- entities,
66- permission_token_rights,
77- phone_rsvps_to_entity,
88-} from "drizzle/schema";
99-import twilio from "twilio";
1010-import { pool } from "supabase/pool";
1111-1212-export async function sendUpdateToRSVPS(
1313- token: { id: string },
1414- {
1515- entity,
1616- message,
1717- eventName,
1818- sendto,
1919- publicLeafletID,
2020- }: {
2121- entity: string;
2222- message: string;
2323- eventName: string;
2424- publicLeafletID: string;
2525- sendto: { GOING: boolean; MAYBE: boolean; NOT_GOING: boolean };
2626- },
2727-) {
2828- let dbclient = await pool.connect();
2929- const db = drizzle(dbclient);
3030- let token_rights = await db
3131- .select()
3232- .from(permission_token_rights)
3333- .where(eq(permission_token_rights.token, token.id));
3434-3535- let RSVPS = db
3636- .select()
3737- .from(phone_rsvps_to_entity)
3838- .innerJoin(entities, eq(phone_rsvps_to_entity.entity, entities.id))
3939- .where(eq(phone_rsvps_to_entity.entity, entity));
4040-4141- dbclient.release();
4242-4343- if (!token_rights[0]?.write) return;
4444- let rsvps = await RSVPS;
4545- let entity_set = rsvps[0]?.entities.set;
4646- if (!token_rights.find((r) => r.entity_set === entity_set)) {
4747- return;
4848- }
4949-5050- const accountSid = process.env.TWILIO_ACCOUNT_SID;
5151- const authToken = process.env.TWILIO_AUTH_TOKEN;
5252- const client = twilio(accountSid, authToken);
5353-5454- for (let rsvp of rsvps) {
5555- if (sendto[rsvp.phone_rsvps_to_entity.status]) {
5656- let { country_code, phone_number } = rsvp.phone_rsvps_to_entity;
5757- let number = `+${country_code}${phone_number}`;
5858- await client.messages.create({
5959- contentSid: "HX8e1217f791d38fa4cf7b7b24a02fe10c",
6060- contentVariables: JSON.stringify({
6161- 1: eventName,
6262- 2: message,
6363- 3: `https://leaflet.pub/${publicLeafletID}`,
6464- }),
6565- from: `${country_code === "1" ? "" : "whatsapp:"}+18449523391`,
6666- messagingServiceSid: "MGffbf9a66770350b25caf3b80b9aac481",
6767- to: country_code === "1" ? number : `whatsapp:${number}`,
6868- });
6969- }
7070- }
7171-}
+1-1
actions/subscriptions/confirmEmailSubscription.ts
···88 facts,
99 permission_tokens,
1010} from "drizzle/schema";
1111-import postgres from "postgres";
1111+1212import type { Fact } from "src/replicache";
1313import { Database } from "supabase/database.types";
1414import { pool } from "supabase/pool";
+1-1
actions/subscriptions/deleteSubscription.ts
···2233import { drizzle } from "drizzle-orm/node-postgres";
44import { email_subscriptions_to_entity, facts } from "drizzle/schema";
55-import postgres from "postgres";
55+66import { eq, and, sql } from "drizzle-orm";
77import type { Fact } from "src/replicache";
88import { v7 } from "uuid";
+1-1
actions/subscriptions/sendPostToSubscribers.ts
···55import { and, eq } from "drizzle-orm";
66import { drizzle } from "drizzle-orm/node-postgres";
77import { email_subscriptions_to_entity, entities } from "drizzle/schema";
88-import postgres from "postgres";
88+99import type { PermissionToken } from "src/replicache";
1010import { Database } from "supabase/database.types";
1111import { pool } from "supabase/pool";
···55import { and, eq } from "drizzle-orm";
66import { drizzle } from "drizzle-orm/node-postgres";
77import { email_subscriptions_to_entity } from "drizzle/schema";
88-import postgres from "postgres";
88+99import { getBlocksWithTypeLocal } from "src/replicache/getBlocks";
1010import type { Fact, PermissionToken } from "src/replicache";
1111import type { Attribute } from "src/replicache/attributes";
···11import { NextRequest } from "next/server";
22import { drizzle } from "drizzle-orm/node-postgres";
33import { email_subscriptions_to_entity } from "drizzle/schema";
44-import postgres from "postgres";
44+55import { eq } from "drizzle-orm";
66import { pool } from "supabase/pool";
77
···22import { useMemo, useState } from "react";
33import { parseColor } from "react-aria-components";
44import { useEntity } from "src/replicache";
55-import { getColorDifference } from "./themeUtils";
65import { useColorAttribute, colorToString } from "./useColorAttribute";
76import { BaseThemeProvider, CardBorderHiddenContext } from "./ThemeProvider";
87import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api";
+82-35
components/ThemeManager/ThemeProvider.tsx
···2828 PublicationBackgroundProvider,
2929 PublicationThemeProvider,
3030} from "./PublicationThemeProvider";
3131-import { getColorDifference } from "./themeUtils";
3232-import { getFontConfig, getGoogleFontsUrl, getFontFamilyValue, getFontBaseSize, defaultFontId } from "src/fonts";
3131+import { compareColors } from "./themeUtils";
3232+import {
3333+ getFontConfig,
3434+ getGoogleFontsUrl,
3535+ getFontFamilyValue,
3636+ getFontBaseSize,
3737+ defaultFontId,
3838+} from "src/fonts";
33393440// define a function to set an Aria Color to a CSS Variable in RGB
3541function setCSSVariableToColor(
···89959096 let pageWidth = useEntity(props.entityID, "theme/page-width");
9197 // Use initial font IDs as fallback until Replicache syncs
9292- let headingFontId = useEntity(props.entityID, "theme/heading-font")?.data.value ?? props.initialHeadingFontId;
9393- let bodyFontId = useEntity(props.entityID, "theme/body-font")?.data.value ?? props.initialBodyFontId;
9898+ let headingFontId =
9999+ useEntity(props.entityID, "theme/heading-font")?.data.value ??
100100+ props.initialHeadingFontId;
101101+ let bodyFontId =
102102+ useEntity(props.entityID, "theme/body-font")?.data.value ??
103103+ props.initialBodyFontId;
9410495105 return (
96106 <CardBorderHiddenContext.Provider value={!!cardBorderHiddenValue}>
···160170 !showPageBackground && !hasBackgroundImage ? bgLeaflet : bgPageProp;
161171162172 let accentContrast;
173173+ let bgRef = colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb");
174174+ let primaryStr = colorToString(primary, "rgb");
175175+163176 let sortedAccents = [accent1, accent2].sort((a, b) => {
164177 // sort accents by contrast against the background
165178 return (
166166- getColorDifference(
167167- colorToString(b, "rgb"),
168168- colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
169169- ) -
170170- getColorDifference(
171171- colorToString(a, "rgb"),
172172- colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
173173- )
179179+ compareColors(colorToString(b, "rgb"), bgRef).distance -
180180+ compareColors(colorToString(a, "rgb"), bgRef).distance
174181 );
175182 });
183183+184184+ let bestVsText = compareColors(
185185+ colorToString(sortedAccents[0], "rgb"),
186186+ primaryStr,
187187+ );
188188+ let altVsBg = compareColors(colorToString(sortedAccents[1], "rgb"), bgRef);
189189+176190 if (
177191 // if the contrast-y accent is too similar to text color
178178- getColorDifference(
179179- colorToString(sortedAccents[0], "rgb"),
180180- colorToString(primary, "rgb"),
181181- ) < 0.15 &&
192192+ // (close in distance AND not distinguishable by hue/chroma)
193193+ bestVsText.distance < 0.15 &&
194194+ bestVsText.chromaDiff < 0.05 &&
182195 // and if the other accent is different enough from the background
183183- getColorDifference(
184184- colorToString(sortedAccents[1], "rgb"),
185185- colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
186186- ) > 0.31
196196+ altVsBg.distance > 0.31
187197 ) {
188198 //then choose the less contrast-y accent
189199 accentContrast = sortedAccents[1];
···192202 accentContrast = sortedAccents[0];
193203 }
194204205205+ // Check if the accent contrast color is visually similar to the text color.
206206+ // We check both overall OKLab distance AND chroma (hue/saturation) difference
207207+ // because dark colors are compressed in OKLab lightness — a dark blue accent
208208+ // vs black text can have a small OKLab distance yet be clearly distinguishable
209209+ // by hue. If the chroma difference is significant, the colors are visually
210210+ // distinct and don't need an underline to tell them apart.
211211+ let accentVsText = compareColors(
212212+ colorToString(accentContrast, "rgb"),
213213+ primaryStr,
214214+ );
215215+ let accentContrastSimilarToText =
216216+ accentVsText.distance < 0.45 && accentVsText.chromaDiff < 0.05;
217217+195218 // Get font configs for CSS variables.
196219 // When using the default font (Quattro), use var(--font-quattro) which is
197220 // always available via next/font/local in layout.tsx, rather than the raw
···203226 const headingFontConfig = getFontConfig(headingFontId);
204227 const bodyFontConfig = getFontConfig(bodyFontId);
205228 const headingFontValue = isDefaultHeading
206206- ? (isDefaultBody ? undefined : "var(--font-quattro)")
229229+ ? isDefaultBody
230230+ ? undefined
231231+ : "var(--font-quattro)"
207232 : getFontFamilyValue(headingFontConfig);
208233 const bodyFontValue = isDefaultBody
209209- ? (isDefaultHeading ? undefined : "var(--font-quattro)")
234234+ ? isDefaultHeading
235235+ ? undefined
236236+ : "var(--font-quattro)"
210237 : getFontFamilyValue(bodyFontConfig);
211211- const bodyFontBaseSize = isDefaultBody ? undefined : getFontBaseSize(bodyFontConfig);
238238+ const bodyFontBaseSize = isDefaultBody
239239+ ? undefined
240240+ : getFontBaseSize(bodyFontConfig);
212241 const headingGoogleFontsUrl = getGoogleFontsUrl(headingFontConfig);
213242 const bodyGoogleFontsUrl = getGoogleFontsUrl(bodyFontConfig);
214243···222251 if (existingLink) return;
223252224253 // Add preconnect hints if not present
225225- if (!document.querySelector('link[href="https://fonts.googleapis.com"]')) {
254254+ if (
255255+ !document.querySelector('link[href="https://fonts.googleapis.com"]')
256256+ ) {
226257 const preconnect1 = document.createElement("link");
227258 preconnect1.rel = "preconnect";
228259 preconnect1.href = "https://fonts.googleapis.com";
···249280250281 loadGoogleFont(headingGoogleFontsUrl, headingFontConfig.fontFamily);
251282 loadGoogleFont(bodyGoogleFontsUrl, bodyFontConfig.fontFamily);
252252- }, [headingGoogleFontsUrl, bodyGoogleFontsUrl, headingFontConfig.fontFamily, bodyFontConfig.fontFamily]);
283283+ }, [
284284+ headingGoogleFontsUrl,
285285+ bodyGoogleFontsUrl,
286286+ headingFontConfig.fontFamily,
287287+ bodyFontConfig.fontFamily,
288288+ ]);
253289254290 useEffect(() => {
255291 if (local) return;
···293329 "--accent-1-is-contrast",
294330 accentContrast === accent1 ? "1" : "0",
295331 );
332332+ el?.style.setProperty(
333333+ "--accent-contrast-similar-to-text",
334334+ accentContrastSimilarToText ? "1" : "0",
335335+ );
336336+ el?.style.setProperty(
337337+ "--link-underline",
338338+ accentContrastSimilarToText ? "underline" : "none",
339339+ );
296340297341 // Set page width CSS variable
298342 el?.style.setProperty(
299343 "--page-width-setting",
300344 (pageWidth || 624).toString(),
301345 );
302302-303346 }, [
304347 local,
305348 bgLeaflet,
···311354 accent1,
312355 accent2,
313356 accentContrast,
357357+ accentContrastSimilarToText,
314358 pageWidth,
315359 ]);
316360 return (
···326370 "--accent-2": colorToString(accent2, "rgb"),
327371 "--accent-contrast": colorToString(accentContrast, "rgb"),
328372 "--accent-1-is-contrast": accentContrast === accent1 ? 1 : 0,
373373+ "--accent-contrast-similar-to-text": accentContrastSimilarToText
374374+ ? 1
375375+ : 0,
376376+ "--link-underline": accentContrastSimilarToText
377377+ ? "underline"
378378+ : "none",
329379 "--highlight-1": highlight1
330380 ? `rgb(${colorToString(parseColor(`hsba(${highlight1})`), "rgb")})`
331381 : "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 75%)",
···336386 "--page-width-units": `min(${pageWidth || 624}px, calc(100vw - 12px))`,
337387 "--theme-heading-font": headingFontValue,
338388 "--theme-font": bodyFontValue,
339339- "--theme-font-base-size": bodyFontBaseSize ? `${bodyFontBaseSize}px` : undefined,
389389+ "--theme-font-base-size": bodyFontBaseSize
390390+ ? `${bodyFontBaseSize}px`
391391+ : undefined,
340392 } as CSSProperties
341393 }
342394 >
···372424 let accentContrast =
373425 bgPage && accent1 && accent2
374426 ? [accent1, accent2].sort((a, b) => {
427427+ let bgStr = colorToString(bgPage, "rgb");
375428 return (
376376- getColorDifference(
377377- colorToString(b, "rgb"),
378378- colorToString(bgPage, "rgb"),
379379- ) -
380380- getColorDifference(
381381- colorToString(a, "rgb"),
382382- colorToString(bgPage, "rgb"),
383383- )
429429+ compareColors(colorToString(b, "rgb"), bgStr).distance -
430430+ compareColors(colorToString(a, "rgb"), bgStr).distance
384431 );
385432 })[0]
386433 : null;
+16-4
components/ThemeManager/themeUtils.ts
···11-import { parse, ColorSpace, sRGB, distance, OKLab } from "colorjs.io/fn";
11+import { parse, ColorSpace, sRGB, distance, OKLab, to } from "colorjs.io/fn";
2233// define the color defaults for everything
44export const ThemeDefaults = {
···1616 "theme/accent-contrast": "#57822B",
1717};
18181919-// used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast
2020-export function getColorDifference(color1: string, color2: string) {
1919+// Compares two RGB color strings in OKLab space and returns both the overall
2020+// perceptual distance and the chroma (hue/saturation) difference.
2121+//
2222+// Why both? Dark colors are compressed in OKLab lightness, so two colors can
2323+// have a small overall distance yet be clearly distinguishable by hue (e.g.
2424+// dark blue vs black). Checking chromaDiff lets callers tell apart "two
2525+// similar grays" from "a dark chromatic color next to a gray/black".
2626+export function compareColors(color1: string, color2: string) {
2127 ColorSpace.register(sRGB);
2228 ColorSpace.register(OKLab);
23292430 let parsedColor1 = parse(`rgb(${color1})`);
2531 let parsedColor2 = parse(`rgb(${color2})`);
26322727- return distance(parsedColor1, parsedColor2, "oklab");
3333+ let [, a1, b1] = to(parsedColor1, "oklab").coords;
3434+ let [, a2, b2] = to(parsedColor2, "oklab").coords;
3535+3636+ return {
3737+ distance: distance(parsedColor1, parsedColor2, "oklab"),
3838+ chromaDiff: Math.sqrt((a1 - a2) ** 2 + (b1 - b2) ** 2),
3939+ };
2840}
+9-38
components/Toolbar/InlineLinkToolbar.tsx
···11import { schema } from "components/Blocks/TextBlock/schema";
22-import { EditorState, TextSelection } from "prosemirror-state";
22+import { TextSelection } from "prosemirror-state";
33import { useUIState } from "src/useUIState";
44import { ToolbarButton } from ".";
55import { useEffect, useState } from "react";
66import { Separator } from "components/Layout";
77-import { MarkType } from "prosemirror-model";
87import { setEditorState, useEditorStates } from "src/state/useEditorState";
98import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark";
99+import { findMarkRange } from "src/utils/prosemirror/findMarkRange";
1010+import { ensureProtocol } from "src/utils/ensureProtocol";
1011import { Input } from "components/Input";
1112import { useReplicache } from "src/replicache";
1213import { CheckTiny } from "components/Icons/CheckTiny";
···8182 start = from;
8283 end = to;
8384 } else {
8484- let markRange = findMarkRange(focusedEditor.editor, schema.marks.link);
8585+ let markRange = findMarkRange(
8686+ focusedEditor.editor.doc,
8787+ schema.marks.link,
8888+ from,
8989+ );
8590 start = markRange.start;
8691 end = markRange.end;
8792 }
···9499 }
95100 let [linkValue, setLinkValue] = useState(content);
96101 let setLink = () => {
9797- let href =
9898- !linkValue.startsWith("http") &&
9999- !linkValue.startsWith("mailto") &&
100100- !linkValue.startsWith("tel:")
101101- ? `https://${linkValue}`
102102- : linkValue;
102102+ let href = ensureProtocol(linkValue);
103103104104 let editor = focusedEditor?.editor;
105105 if (!editor || start === null || !end || !focusedBlock) return;
···176176 );
177177}
178178179179-function findMarkRange(state: EditorState, markType: MarkType) {
180180- const { from, $from } = state.selection;
181181-182182- // Find the start of the mark
183183- let start = from;
184184- let startPos = $from;
185185- while (
186186- startPos.parent.inlineContent &&
187187- startPos.nodeBefore &&
188188- startPos.nodeBefore.marks.some((mark) => mark.type === markType)
189189- ) {
190190- start -= startPos.nodeBefore.nodeSize;
191191- startPos = state.doc.resolve(start);
192192- }
193193-194194- // Find the end of the mark
195195- let end = from;
196196- let endPos = $from;
197197- while (
198198- endPos.parent.inlineContent &&
199199- endPos.nodeAfter &&
200200- endPos.nodeAfter.marks.some((mark) => mark.type === markType)
201201- ) {
202202- end += endPos.nodeAfter.nodeSize;
203203- endPos = state.doc.resolve(end);
204204- }
205205-206206- return { start, end };
207207-}
···11761176 type: 'ref',
11771177 ref: 'lex:pub.leaflet.blocks.image#aspectRatio',
11781178 },
11791179+ fullBleed: {
11801180+ type: 'boolean',
11811181+ description:
11821182+ 'Whether the image should extend to the full width of the container, ignoring padding.',
11831183+ },
11791184 },
11801185 },
11811186 aspectRatio: {
···12331238 type: 'object',
12341239 required: ['content'],
12351240 properties: {
12411241+ checked: {
12421242+ type: 'boolean',
12431243+ description:
12441244+ 'If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item.',
12451245+ },
12361246 content: {
12371247 type: 'union',
12381248 refs: [
···13381348 type: 'object',
13391349 required: ['content'],
13401350 properties: {
13511351+ checked: {
13521352+ type: 'boolean',
13531353+ description:
13541354+ 'If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item.',
13551355+ },
13411356 content: {
13421357 type: 'union',
13431358 refs: [
···21522167 type: 'array',
21532168 items: {
21542169 type: 'ref',
21552155- ref: 'lex:pub.leaflet.richtext.facet',
21702170+ ref: 'lex:pub.leaflet.richtext.facet#main',
21562171 },
21572172 },
21582173 },
+2
lexicons/api/types/pub/leaflet/blocks/image.ts
···2020 /** Alt text description of the image, for accessibility. */
2121 alt?: string
2222 aspectRatio: AspectRatio
2323+ /** Whether the image should extend to the full width of the container, ignoring padding. */
2424+ fullBleed?: boolean
2325}
24262527const hashMain = 'main'
···37373838export interface ListItem {
3939 $type?: 'pub.leaflet.blocks.orderedList#listItem'
4040+ /** If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item. */
4141+ checked?: boolean
4042 content:
4143 | $Typed<PubLeafletBlocksText.Main>
4244 | $Typed<PubLeafletBlocksHeader.Main>
···35353636export interface ListItem {
3737 $type?: 'pub.leaflet.blocks.unorderedList#listItem'
3838+ /** If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item. */
3939+ checked?: boolean
3840 content:
3941 | $Typed<PubLeafletBlocksText.Main>
4042 | $Typed<PubLeafletBlocksHeader.Main>
+4
lexicons/pub/leaflet/blocks/image.json
···2323 "aspectRatio": {
2424 "type": "ref",
2525 "ref": "#aspectRatio"
2626+ },
2727+ "fullBleed": {
2828+ "type": "boolean",
2929+ "description": "Whether the image should extend to the full width of the container, ignoring padding."
2630 }
2731 }
2832 },
+4
lexicons/pub/leaflet/blocks/orderedList.json
···2727 "content"
2828 ],
2929 "properties": {
3030+ "checked": {
3131+ "type": "boolean",
3232+ "description": "If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item."
3333+ },
3034 "content": {
3135 "type": "union",
3236 "refs": [
+4
lexicons/pub/leaflet/blocks/unorderedList.json
···2323 "content"
2424 ],
2525 "properties": {
2626+ "checked": {
2727+ "type": "boolean",
2828+ "description": "If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item."
2929+ },
2630 "content": {
2731 "type": "union",
2832 "refs": [
···180180 type: "ref",
181181 ref: "#aspectRatio",
182182 },
183183+ fullBleed: {
184184+ type: "boolean",
185185+ description:
186186+ "Whether the image should extend to the full width of the container, ignoring padding.",
187187+ },
183188 },
184189 },
185190 aspectRatio: {
···203208 properties: {
204209 startIndex: {
205210 type: "integer",
206206- description: "The starting number for this ordered list. Defaults to 1 if not specified.",
211211+ description:
212212+ "The starting number for this ordered list. Defaults to 1 if not specified.",
207213 },
208214 children: { type: "array", items: { type: "ref", ref: "#listItem" } },
209215 },
···212218 type: "object",
213219 required: ["content"],
214220 properties: {
221221+ checked: {
222222+ type: "boolean",
223223+ description:
224224+ "If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item.",
225225+ },
215226 content: {
216227 type: "union",
217228 refs: [
···222233 },
223234 children: {
224235 type: "array",
225225- description: "Nested ordered list items. Mutually exclusive with unorderedListChildren; if both are present, children takes precedence.",
236236+ description:
237237+ "Nested ordered list items. Mutually exclusive with unorderedListChildren; if both are present, children takes precedence.",
226238 items: { type: "ref", ref: "#listItem" },
227239 },
228240 unorderedListChildren: {
229241 type: "ref",
230230- description: "A nested unordered list. Mutually exclusive with children; if both are present, children takes precedence.",
242242+ description:
243243+ "A nested unordered list. Mutually exclusive with children; if both are present, children takes precedence.",
231244 ref: "pub.leaflet.blocks.unorderedList",
232245 },
233246 },
···250263 type: "object",
251264 required: ["content"],
252265 properties: {
266266+ checked: {
267267+ type: "boolean",
268268+ description:
269269+ "If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item.",
270270+ },
253271 content: {
254272 type: "union",
255273 refs: [
···260278 },
261279 children: {
262280 type: "array",
263263- description: "Nested unordered list items. Mutually exclusive with orderedListChildren; if both are present, children takes precedence.",
281281+ description:
282282+ "Nested unordered list items. Mutually exclusive with orderedListChildren; if both are present, children takes precedence.",
264283 items: { type: "ref", ref: "#listItem" },
265284 },
266285 orderedListChildren: {
267286 type: "ref",
268268- description: "Nested ordered list items. Mutually exclusive with children; if both are present, children takes precedence.",
287287+ description:
288288+ "Nested ordered list items. Mutually exclusive with children; if both are present, children takes precedence.",
269289 ref: "pub.leaflet.blocks.orderedList",
270290 },
271291 },