@openpresentation/opf-editor 0.17.0 → 0.18.1

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.
@@ -1,7 +1,7 @@
1
1
  // Design controls (RR-06): the DOM panel for the dimension switches, the design options, block
2
2
  // conversion, chart type and table style/merge. Every control commits one validated, undoable
3
- // change through the session (switchDimension, setDesignOption, setLogoVariant,
4
- // setHeaderFooterZone, convertBlock, setTableStyle, mergeTableCells, ...), so a host that already
3
+ // change through the session (switchDimension, setDesignOption, setOrganizationLogo,
4
+ // setHeaderFooterZone, insertZoneLogo, convertBlock, setTableStyle, mergeTableCells, ...), so a host that already
5
5
  // subscribes to the session redraws on every change, on Undo and on Redo. The panel owns no
6
6
  // document state: it reads the session, mirrors it into native form controls, and reports
7
7
  // problems in a live region. Importing this module does not need a DOM; mounting does.
@@ -20,20 +20,26 @@ import { BLOCK_KIND_LABELS, blockConversionTargets, blockPathForSelection, readB
20
20
  import {
21
21
  DATE_FORMAT_TOKENS,
22
22
  HEADER_FOOTER_ZONES,
23
- LOGO_VARIANTS,
23
+ LOGO_BACKGROUNDS,
24
+ LOGO_SHAPES,
24
25
  ZONE_VALUES,
25
26
  designWarnings,
26
27
  getDesignOption,
27
28
  headerFooterState,
29
+ insertZoneLogo,
28
30
  insertZoneValue,
29
- prepareHeaderFooterZone,
30
- prepareLogoVariant,
31
+ listOrganizations,
32
+ logoReference,
33
+ parseLogoReference,
31
34
  prepareDesignOption,
35
+ prepareHeaderFooterZone,
36
+ prepareOrganizationLogo,
32
37
  readHeaderFooterZone,
33
- readLogoVariants,
38
+ readLogoChoice,
39
+ readOrganizationLogo,
34
40
  setDesignOption,
35
41
  setHeaderFooterZone,
36
- setLogoVariant,
42
+ setOrganizationLogo,
37
43
  } from "./design-options.js";
38
44
  import { createContentControls } from "./content-controls.js";
39
45
  import { IMAGE_EDGES, IMAGE_FITS, IMAGE_SHAPES, readImageTreatments, setImageTreatment } from "./image-options.js";
@@ -57,19 +63,18 @@ const SECTION_TITLES = {
57
63
  "slide-content": "Slide structure",
58
64
  };
59
65
  const OPEN_BY_DEFAULT = new Set(["look", "selection", "image", "table"]);
60
- const LOGO_VARIANT_LABELS = {
61
- default: "Default",
62
- light: "Light (for dark backgrounds)",
63
- dark: "Dark (for light backgrounds)",
64
- stacked: "Stacked",
65
- stackedLight: "Stacked, light",
66
- stackedDark: "Stacked, dark",
67
- icon: "Icon",
68
- iconLight: "Icon, light",
69
- iconDark: "Icon, dark",
70
- wordmark: "Wordmark",
71
- wordmarkLight: "Wordmark, light",
72
- wordmarkDark: "Wordmark, dark",
66
+ // RR-71: the four shapes of an organization's logo, and the two backgrounds a shape can be split by.
67
+ const LOGO_SHAPE_LABELS = {
68
+ all: "All shapes (one image)",
69
+ full: "Full (mark and name side by side)",
70
+ stacked: "Stacked (mark above the name)",
71
+ icon: "Icon (the mark alone)",
72
+ wordmark: "Wordmark (the name alone)",
73
+ };
74
+ const LOGO_BACKGROUND_LABELS = {
75
+ both: "Both backgrounds",
76
+ onLight: "Light backgrounds (usually dark artwork)",
77
+ onDark: "Dark backgrounds (usually light artwork)",
73
78
  };
74
79
  const CELL_FILLS = [
75
80
  ["", "No fill"],
@@ -667,7 +672,7 @@ export function createDesignControls(container, options = {}) {
667
672
  { value: "footer", label: "Footer" },
668
673
  ]);
669
674
  furniture.set(pick.which);
670
- const zoneSelect = selectField("hf-zone", "Zone", { help: "Each zone stacks its parts top to bottom: logo, image, text, social profiles, date." });
675
+ const zoneSelect = selectField("hf-zone", "Zone", { help: "A zone lays its parts out side by side, from the edge it sits on: image (or logo), text, social profiles, date." });
671
676
  zoneSelect.setOptions(HEADER_FOOTER_ZONES.map((value) => ({ value, label: titleCase(value) })));
672
677
  zoneSelect.set(pick.zone);
673
678
  furniture.select.addEventListener("change", () => {
@@ -720,12 +725,25 @@ export function createDesignControls(container, options = {}) {
720
725
  box.focus();
721
726
  box.setSelectionRange?.(caret, caret);
722
727
  });
723
- const logo = checkField("hf-logo", "Show the logo", { onChange: (checked) => edit({ logo: checked }, `${where()} logo ${checked ? "shown" : "removed"}.`) });
724
728
  const image = imageCluster("hf-image", "Image", {
725
729
  apply: (ref) => setHeaderFooterZone(editor, which(), zone(), { image: ref }, scoped()),
726
730
  build: (ref, doc) => prepareHeaderFooterZone(doc, which(), zone(), { image: ref }, scoped()),
727
- help: "A picture in this zone, such as a partner mark or badge. An asset reference, a web address or a data address. Press Enter to apply; clear the field to remove it.",
731
+ help: "A picture in this zone, such as a partner mark or badge: an asset reference, a web address or a data address, or an organization logo (Insert logo, below). Press Enter to apply; clear the field to remove it.",
728
732
  });
733
+ // RR-71: a logo is the zone's image, written as a reference to the organization's logo (var:organization.logo.icon). Which artwork is drawn (light or dark) follows each slide's background.
734
+ const logoShape = selectField("hf-logo-shape", "Logo shape", { help: "Icon is the mark alone and fits a zone best." });
735
+ logoShape.setOptions(LOGO_SHAPES.map((value) => ({ value, label: LOGO_SHAPE_LABELS[value] })));
736
+ logoShape.set("icon");
737
+ const logoOrganization = selectField("hf-logo-organization", "Logo of", { empty: "Primary organization", help: "Choose another organization when the deck has several." });
738
+ const insertLogoButton = button("Insert logo", () => {
739
+ const shape = logoShape.select.value;
740
+ const organization = logoOrganization.select.value || undefined;
741
+ run(() => insertZoneLogo(editor, which(), zone(), { ...scoped(), shape, ...(organization ? { organization } : {}) }), `${where()} now shows the ${shape} logo (${logoReference({ ...(organization ? { organization } : {}), ...(shape !== "full" ? { shape } : {}) })}), replacing any image.`);
742
+ });
743
+ insertLogoButton.dataset.action = "insert-logo";
744
+ const logoHelp = h("p", { class: "opf-dc-help", id: `${uid}-hf-logo-help` }, "Puts the organization's logo in this zone as its image. The logo comes from the organization's logo settings; add it under Logo, watermark and bullets.");
745
+ insertLogoButton.setAttribute("aria-describedby", logoHelp.id);
746
+ const logo = { wrap: h("fieldset", { class: "opf-dc-fieldset", "data-role": "hf-logo" }, h("legend", {}, "Logo"), logoShape.wrap, logoOrganization.wrap, h("div", { class: "opf-dc-actions" }, insertLogoButton), logoHelp) };
729
747
  const dateNow = checkField("hf-date-now", "Show the current date", {
730
748
  help: "Needs the host to supply today's date when it renders or exports; otherwise the part is reported as unresolved.",
731
749
  onChange: (checked) => edit({ date: checked }, `${where()} date ${checked ? "shown" : "removed"}.`),
@@ -764,14 +782,24 @@ export function createDesignControls(container, options = {}) {
764
782
  text.input.value = textValue;
765
783
  }
766
784
  text.set(textValue);
767
- logo.set(fields.logo === true);
768
785
  image.set(stringOf(fields.image), "", `${target()}:${which()}:${zone()}`);
786
+ // The logo menus follow the organizations, and show the logo the zone already has.
787
+ const organizations = listOrganizations(editor.presentation);
788
+ logoOrganization.setOptions(organizations.filter((entry) => entry.id).map((entry) => ({ value: entry.id, label: entry.name ? `${entry.name} (${entry.id})` : entry.id })));
789
+ logoOrganization.wrap.hidden = organizations.length < 2;
790
+ const shown = parseLogoReference(fields.image);
791
+ if (shown) {
792
+ logoShape.set(shown.shape ?? "full");
793
+ logoOrganization.set(shown.organization ?? "");
794
+ }
795
+
769
796
  dateNow.set(fields.date === true);
770
797
  dateFixed.set(typeof fields.date === "string" ? fields.date : "");
771
798
  dateFormat.set(typeof fields.dateFormat === "string" ? fields.dateFormat : "");
772
799
  socials.set(fields.socials === true);
773
800
  insertValue.set("");
774
801
  for (const control of controls) for (const input of control.wrap.querySelectorAll("input,select,textarea,button")) input.disabled = state.hidden;
802
+ insertLogoButton.disabled = state.hidden || !listOrganizations(editor.presentation).length;
775
803
  summary.replaceChildren(
776
804
  ...HEADER_FOOTER_ZONES.map((name) => {
777
805
  const zoneFields = readHeaderFooterZone(editor.presentation, which(), name, scoped());
@@ -786,20 +814,109 @@ export function createDesignControls(container, options = {}) {
786
814
 
787
815
  if (built.brand) {
788
816
  const { body } = built.brand;
789
- const variant = selectField("logo-variant", "Logo variant", { help: "Choose the variant to edit. Engines pick one by background: light on dark, dark on light, then the default." });
790
- variant.setOptions(LOGO_VARIANTS.map((value) => ({ value, label: LOGO_VARIANT_LABELS[value] })));
791
- variant.set("default");
792
- const logoSource = imageCluster("logo", "Logo", {
793
- apply: (ref) => setLogoVariant(editor, variant.select.value, ref, scoped()),
794
- build: (ref, doc) => prepareLogoVariant(doc, variant.select.value, ref, scoped()),
795
- help: "An asset reference, a web address or a data address for the selected variant. Press Enter to apply; clear the field to remove the variant.",
817
+ // RR-71: logos live on the organization. One image serves every shape, or each of the four shapes (full, stacked, icon,
818
+ // wordmark) has its own, optionally split by the background it is drawn on (light or dark).
819
+ const logoOrganizationPick = selectField("org-logo-organization", "Organization to edit", {
820
+ help: "Whose logo to edit. The primary organization's full logo is drawn on cover and section slides.",
821
+ onChange: () => sync(),
822
+ });
823
+ const logoShapePick = selectField("org-logo-shape", "Shape to edit", {
824
+ help: "All shapes sets one image for every place the logo is drawn. Set a shape to give it its own image; a missing shape falls back to the full logo.",
825
+ onChange: () => sync(),
796
826
  });
797
- variant.select.addEventListener("change", () => sync());
798
- const orgLogo = imageCluster("org-logo", "Organization logo (whole presentation)", {
799
- apply: (ref) => setDesignOption(editor, "organizationLogo", ref),
800
- build: (ref, doc) => prepareDesignOption(doc, "organizationLogo", ref),
801
- help: "The primary organization's logo is the fallback wherever the logo is drawn.",
827
+ logoShapePick.setOptions(["all", ...LOGO_SHAPES].map((value) => ({ value, label: LOGO_SHAPE_LABELS[value] })));
828
+ logoShapePick.set("all");
829
+ const logoBackgroundPick = selectField("org-logo-background", "Background to edit", {
830
+ help: "Use a different image on light and on dark backgrounds. Each slide draws the one that suits its background; a missing one uses the other.",
831
+ onChange: () => sync(),
802
832
  });
833
+ logoBackgroundPick.setOptions(LOGO_BACKGROUNDS.map((value) => ({ value, label: LOGO_BACKGROUND_LABELS[value] })));
834
+ logoBackgroundPick.set("both");
835
+ const logoTarget = () => ({ shape: logoShapePick.select.value, background: logoShapePick.select.value === "all" ? "both" : logoBackgroundPick.select.value, organization: Number(logoOrganizationPick.select.value || 0) });
836
+ const orgLogo = imageCluster("org-logo", "Organization logo", {
837
+ apply: (ref) => {
838
+ const { shape, ...rest } = logoTarget();
839
+ return setOrganizationLogo(editor, shape, ref, rest);
840
+ },
841
+ build: (ref, doc) => {
842
+ const { shape, ...rest } = logoTarget();
843
+ return prepareOrganizationLogo(doc, shape, ref, rest);
844
+ },
845
+ help: "An asset reference, a web address or a data address for the chosen shape and background. Press Enter to apply; clear the field to remove it.",
846
+ });
847
+ const logoSummary = h("ul", { class: "opf-dc-summary", "aria-label": "Logo shapes set" });
848
+ const organizationLogoGroup = h("fieldset", { class: "opf-dc-fieldset", "data-role": "organization-logo" }, h("legend", {}, "Organization logo"), logoOrganizationPick.wrap, logoShapePick.wrap, logoBackgroundPick.wrap, orgLogo.wrap, logoSummary);
849
+ const logoSummaryText = (state) => {
850
+ if (!state) return ["No organization yet: add one to give it a logo."];
851
+ if (state.all !== undefined) return ["One image for every shape."];
852
+ const rows = LOGO_SHAPES.filter((shape) => state.shapes[shape]).map((shape) => {
853
+ const entry = state.shapes[shape];
854
+ return `${titleCase(shape)}: ${entry.both !== undefined ? "both backgrounds" : [entry.onLight !== undefined ? "light backgrounds" : "", entry.onDark !== undefined ? "dark backgrounds" : ""].filter(Boolean).join(" and ")}`;
855
+ });
856
+ return rows.length ? rows : ["No logo yet."];
857
+ };
858
+ const syncOrganizationLogo = () => {
859
+ const organizations = listOrganizations(editor.presentation);
860
+ const wanted = logoOrganizationPick.select.value;
861
+ logoOrganizationPick.setOptions(organizations.map((entry) => ({ value: String(entry.index), label: `${entry.name ?? entry.id ?? `Organization ${entry.index + 1}`}${entry.primary ? " (primary)" : ""}` })));
862
+ const fallback = organizations.find((entry) => entry.primary) ?? organizations[0];
863
+ logoOrganizationPick.set(organizations.some((entry) => String(entry.index) === wanted) ? wanted : String(fallback?.index ?? ""));
864
+ logoOrganizationPick.wrap.hidden = organizations.length < 2;
865
+ const all = logoShapePick.select.value === "all";
866
+ logoBackgroundPick.wrap.hidden = all;
867
+ const state = organizations.length ? readOrganizationLogo(editor.presentation, { organization: Number(logoOrganizationPick.select.value) }) : null;
868
+ const { shape, background } = logoTarget();
869
+ const entry = shape === "all" ? undefined : state?.shapes[shape];
870
+ let value = "";
871
+ let note = "";
872
+ if (shape === "all") value = stringOf(state?.all);
873
+ else if (entry) value = stringOf(background === "both" ? entry.both : entry[background]);
874
+ else if (state?.all !== undefined) note = "uses the image for all shapes";
875
+ if (shape !== "all" && entry?.both !== undefined && background !== "both") note = "uses the image for both backgrounds";
876
+ if (shape !== "all" && background === "both" && entry && entry.both === undefined) note = "set per background";
877
+ orgLogo.set(value, note, `logo:${logoOrganizationPick.select.value}:${shape}:${background}`);
878
+ for (const input of [...orgLogo.wrap.querySelectorAll("input"), ...logoShapePick.wrap.querySelectorAll("select"), ...logoBackgroundPick.wrap.querySelectorAll("select")]) input.disabled = !organizations.length;
879
+ logoSummary.replaceChildren(...logoSummaryText(state).map((line) => h("li", {}, line)));
880
+ };
881
+ // The logo that covers, sections and picture bullets draw: the primary organization's by default, another organization's or shape, or none.
882
+ const choiceOrganizationPick = selectField("design-logo-organization", "Logo for covers, sections and bullets", {
883
+ empty: "Primary organization",
884
+ help: "Which organization's logo covers and sections draw, and picture bullets use. Choose None to draw no logo.",
885
+ onChange: () => applyLogoChoice(),
886
+ });
887
+ const choiceShapePick = selectField("design-logo-shape", "Shape to draw", {
888
+ empty: "Default (full; icon for bullets)",
889
+ help: "Choose a shape to draw it instead of the default for that place. A missing shape falls back to the full logo.",
890
+ onChange: () => applyLogoChoice(),
891
+ });
892
+ choiceShapePick.setOptions(LOGO_SHAPES.map((value) => ({ value, label: LOGO_SHAPE_LABELS[value] })));
893
+ const logoChoiceGroup = h("fieldset", { class: "opf-dc-fieldset", "data-role": "design-logo" }, h("legend", {}, "Which logo to draw"), choiceOrganizationPick.wrap, choiceShapePick.wrap);
894
+ const applyLogoChoice = () => {
895
+ const organization = choiceOrganizationPick.select.value;
896
+ const shape = choiceShapePick.select.value || undefined;
897
+ let value;
898
+ if (organization === "none") value = false;
899
+ else if (organization === "" && shape === undefined) value = null;
900
+ else value = logoReference({ ...(organization !== "" && organization !== "primary" ? { organization } : {}), ...(shape ? { shape } : {}) });
901
+ run(() => setDesignOption(editor, "logo", value, scoped()), value === null ? "Logo set to the primary organization's." : value === false ? "No logo is drawn." : `Logo set to ${value}.`);
902
+ };
903
+ const syncLogoChoice = () => {
904
+ const choice = readLogoChoice(editor.presentation, scoped());
905
+ const entries = listOrganizations(editor.presentation)
906
+ .filter((entry) => entry.id)
907
+ .map((entry) => ({ value: entry.id, label: `${entry.name ?? entry.id}${entry.primary ? " (primary)" : ""}` }));
908
+ // On a slide, an explicit reference to the primary organization overrides a logo setting the presentation made.
909
+ if (state.scope === "slide" && editor.presentation.design?.logo !== undefined) entries.unshift({ value: "primary", label: "Primary organization (instead of the presentation's setting)" });
910
+ entries.push({ value: "none", label: "None" });
911
+ choiceOrganizationPick.setOptions(entries);
912
+ let organization = "";
913
+ if (choice.mode === "none") organization = "none";
914
+ else if (choice.mode === "custom") organization = String(choice.value);
915
+ else if (choice.mode === "reference") organization = choice.organization ?? (state.scope === "slide" && choice.shape === undefined ? "primary" : "");
916
+ choiceOrganizationPick.set(organization, sourceNote(choice.scope, choice.value !== undefined));
917
+ choiceShapePick.set(choice.mode === "reference" ? (choice.shape ?? "") : "");
918
+ for (const input of choiceShapePick.wrap.querySelectorAll("select")) input.disabled = choice.mode === "none";
919
+ };
803
920
  const bullet = selectField("list-bullet", "List bullets", {
804
921
  empty: "Default (character)",
805
922
  help: "Picture bullets draw the logo as the marker and need a logo.",
@@ -839,16 +956,12 @@ export function createDesignControls(container, options = {}) {
839
956
  const watermarkOff = checkField("watermark-off", "Hide the inherited watermark", {
840
957
  onChange: (checked) => run(() => setDesignOption(editor, "watermark", checked ? false : null, scoped()), checked ? "Watermark hidden." : "Watermark restored."),
841
958
  });
842
- body.append(variant.wrap, logoSource.wrap, orgLogo.wrap, bullet.wrap, accent.wrap, watermarkSource.wrap, watermarkText.wrap, watermarkOpacity.wrap, watermarkOff.wrap);
959
+ body.append(organizationLogoGroup, logoChoiceGroup, bullet.wrap, accent.wrap, watermarkSource.wrap, watermarkText.wrap, watermarkOpacity.wrap, watermarkOff.wrap);
843
960
  const warningList = h("ul", { class: "opf-dc-warnings", "aria-label": "Logo warnings" });
844
961
  body.append(warningList);
845
962
  syncs.push(() => {
846
- const variants = readLogoVariants(editor.presentation, scoped());
847
- logoSource.set(stringOf(variants[variant.select.value]), "", `${target()}:${variant.select.value}`);
848
- const organization = editor.presentation.organization;
849
- const owner = Array.isArray(organization) ? organization[0] : organization;
850
- orgLogo.set(stringOf(owner?.logo), "", "org");
851
- for (const input of orgLogo.wrap.querySelectorAll("input")) input.disabled = !owner;
963
+ syncOrganizationLogo();
964
+ syncLogoChoice();
852
965
  const bulletOption = getDesignOption(editor.presentation, "listBullet", scoped());
853
966
  bullet.set(bulletOption.value ?? "", sourceNote(bulletOption.scope, bulletOption.value !== undefined));
854
967
  const accentOption = getDesignOption(editor.presentation, "accentFont", scoped());
@@ -863,7 +976,7 @@ export function createDesignControls(container, options = {}) {
863
976
  const own = scopeIndex() === undefined ? editor.presentation.design?.watermark : editor.presentation.slides?.[scopeIndex()]?.design?.watermark;
864
977
  watermarkOff.set(own === false);
865
978
  watermarkOff.wrap.hidden = state.scope !== "slide";
866
- warningList.replaceChildren(...designWarnings(editor.presentation, getSlide()).filter((warning) => warning.path === "design.listBullet").map((warning) => h("li", {}, warning.message)));
979
+ warningList.replaceChildren(...designWarnings(editor.presentation, getSlide()).filter((warning) => warning.path === "design.listBullet" || /^(slides\.\d+\.)?design\.logo$/.test(warning.path)).map((warning) => h("li", {}, warning.message)));
867
980
  });
868
981
  }
869
982
 
@@ -10,22 +10,25 @@ export type DesignOptionId =
10
10
  | "imageFit"
11
11
  | "accentFont"
12
12
  | "logo"
13
- | "organizationLogo"
14
13
  | "watermark";
15
14
  export interface DesignOptionDescriptor {
16
15
  id: DesignOptionId;
17
16
  label: string;
18
- type: "enum" | "boolean" | "font" | "logo" | "organization-logo" | "watermark";
17
+ type: "enum" | "boolean" | "font" | "logo" | "watermark";
19
18
  /** Allowed values of an enum option. */
20
19
  values?: readonly string[];
21
20
  scopes: ("deck" | "slide")[];
22
21
  path: string;
23
22
  }
24
23
  export declare const DESIGN_OPTIONS: readonly DesignOptionDescriptor[];
25
- export declare const LOGO_VARIANTS: readonly ["default", "light", "dark", "stacked", "stackedLight", "stackedDark", "icon", "iconLight", "iconDark", "wordmark", "wordmarkLight", "wordmarkDark"];
26
- export type LogoVariant = (typeof LOGO_VARIANTS)[number];
27
- /** Every field a header or footer zone can hold. */
28
- export declare const ZONE_FIELDS: readonly ["logo", "text", "image", "date", "dateFormat", "socials"];
24
+ /** The shapes an organization's logo can have (core's `LOGO_SHAPES`). */
25
+ export declare const LOGO_SHAPES: readonly ["full", "stacked", "icon", "wordmark"];
26
+ export type LogoShape = (typeof LOGO_SHAPES)[number];
27
+ /** The backgrounds a logo shape can be split by: one image for `both`, or one for `onLight` and one for `onDark` backgrounds. */
28
+ export declare const LOGO_BACKGROUNDS: readonly ["both", "onLight", "onDark"];
29
+ export type LogoBackground = (typeof LOGO_BACKGROUNDS)[number];
30
+ /** Every field a header or footer zone can hold. The logo is the zone's `image` (`var:organization.logo.icon`, see `insertZoneLogo`). */
31
+ export declare const ZONE_FIELDS: readonly ["text", "image", "date", "dateFormat", "socials"];
29
32
  /** One entry of a zone's "Insert value" menu: the built-in variable `name`, its menu `label` and the `token` (`{{name}}`) that lands in the zone's `text`. */
30
33
  export interface ZoneValue {
31
34
  readonly name: "slide.number" | "deck.slideCount" | "slide.section" | "organization.name" | "speaker.name" | "deck.name";
@@ -40,12 +43,10 @@ export declare const HEADER_FOOTER_ZONES: readonly ["left", "center", "right"];
40
43
  export type HeaderFooterZone = (typeof HEADER_FOOTER_ZONES)[number];
41
44
 
42
45
  export interface DesignOptionOptions {
43
- /** One slide instead of the deck. Not allowed for organizationLogo. */
46
+ /** One slide instead of the deck. */
44
47
  slideIndex?: number;
45
48
  /** Deck scope: also remove slide-level values that would hide the change. */
46
49
  clearSlideOverrides?: boolean;
47
- /** organizationLogo with several organizations (default 0). */
48
- index?: number;
49
50
  /** Session change metadata (session forms only). */
50
51
  meta?: Record<string, unknown>;
51
52
  }
@@ -58,15 +59,14 @@ export interface PreparedDesignOption {
58
59
  option: string;
59
60
  scope: "deck" | "slide";
60
61
  slideIndex?: number;
61
- /** The logo variant or header/footer zone an edit named. */
62
- variant?: string;
62
+ /** The header/footer zone an edit named. */
63
63
  zone?: string;
64
64
  presentation: unknown;
65
65
  patches: JsonPatchOperation[];
66
66
  changed: boolean;
67
67
  /** Slides whose own design hides a deck-level change. */
68
68
  shadowed: number[];
69
- /** Settings that need a logo the document does not have. */
69
+ /** Settings that need a logo or other content the document does not have. */
70
70
  warnings: DesignWarning[];
71
71
  }
72
72
  export interface DesignOptionChange extends Omit<EditorChange, "presentation" | "patches"> {
@@ -85,14 +85,60 @@ export declare function prepareDesignOption(presentation: unknown, option: Desig
85
85
  /** Set one option as a single undoable transaction. */
86
86
  export declare function setDesignOption(editor: EditorSession, option: DesignOptionId, value: unknown, options?: DesignOptionOptions): DesignOptionChange;
87
87
  /** `{ value, scope, inherited }`; scope is "slide", "deck" or "default". */
88
- export declare function getDesignOption(presentation: unknown, option: DesignOptionId, options?: Pick<DesignOptionOptions, "slideIndex" | "index">): { value: unknown; scope: "slide" | "deck" | "default"; inherited: boolean };
89
- export declare function prepareLogoVariant(presentation: unknown, variant: LogoVariant, source: string | Record<string, unknown> | null, options?: DesignOptionOptions): PreparedDesignOption;
90
- /** Set or clear one `design.logo` variant; a lone default stays a bare source. */
91
- export declare function setLogoVariant(editor: EditorSession, variant: LogoVariant, source: string | Record<string, unknown> | null, options?: DesignOptionOptions): DesignOptionChange;
92
- /** The variants `design.logo` sets at a scope (a bare logo is reported as `default`). */
93
- export declare function readLogoVariants(presentation: unknown, options?: Pick<DesignOptionOptions, "slideIndex">): Partial<Record<LogoVariant, unknown>>;
88
+ export declare function getDesignOption(presentation: unknown, option: DesignOptionId, options?: Pick<DesignOptionOptions, "slideIndex">): { value: unknown; scope: "slide" | "deck" | "default"; inherited: boolean };
89
+ /** A parsed `var:organization(.<id>)?.logo(.<shape>)?` reference: the organization's id (absent: the primary organization) and the shape when the reference names one. */
90
+ export interface LogoReference {
91
+ organization?: string;
92
+ shape?: LogoShape;
93
+ }
94
+ /** Parse a logo reference, or null for any other string. */
95
+ export declare function parseLogoReference(text: unknown): LogoReference | null;
96
+ /** Write a logo reference: `logoReference({ organization: "beta", shape: "icon" })` is `"var:organization.beta.logo.icon"`. */
97
+ export declare function logoReference(reference?: LogoReference): string;
98
+ /** What `design.logo` takes: `null` (unset: the primary organization's logo), `false` (no logo), a reference, or the organization and shape to write as one. */
99
+ export type LogoChoice = null | false | string | LogoReference;
100
+ export interface OrganizationEntry {
101
+ index: number;
102
+ /** What a `var:` reference addresses; an organization without one is only reachable as the primary organization. */
103
+ id?: string;
104
+ name?: string;
105
+ /** The organization the unset logo and `var:organization.logo` mean (`role: "primary"`, else the first). */
106
+ primary: boolean;
107
+ hasLogo: boolean;
108
+ }
109
+ /** The deck's organizations (a single organization object is a list of one). */
110
+ export declare function listOrganizations(presentation: unknown): OrganizationEntry[];
111
+ export interface OrganizationLogoOptions extends Omit<DesignOptionOptions, "slideIndex" | "clearSlideOverrides"> {
112
+ /** The organization's index or id (default: the primary organization). */
113
+ organization?: number | string;
114
+ /** `both` (default): one image for the shape. `onLight` or `onDark`: the image for that background only. Not with shape `all`. */
115
+ background?: LogoBackground;
116
+ }
117
+ /** Compute the patch that sets or clears (`null`) the organization's logo: `all` is one image for every shape, otherwise one shape (for one background or both). */
118
+ export declare function prepareOrganizationLogo(presentation: unknown, shape: "all" | LogoShape, source: string | Record<string, unknown> | null, options?: Omit<OrganizationLogoOptions, "meta">): PreparedDesignOption & { shape: "all" | LogoShape; background: LogoBackground; organization: number };
119
+ /** Set or clear the organization's logo as a single undoable transaction. */
120
+ export declare function setOrganizationLogo(editor: EditorSession, shape: "all" | LogoShape, source: string | Record<string, unknown> | null, options?: OrganizationLogoOptions): DesignOptionChange;
121
+ export interface OrganizationLogoState {
122
+ organization: OrganizationEntry;
123
+ /** The bare image when one image serves every shape. */
124
+ all?: unknown;
125
+ /** The shapes the logo sets: `{ both }` for one image, or `{ onLight?, onDark? }`. */
126
+ shapes: Partial<Record<LogoShape, { both?: unknown; onLight?: unknown; onDark?: unknown }>>;
127
+ }
128
+ /** An organization's logo as a panel shows it; null when the deck has no organization. */
129
+ export declare function readOrganizationLogo(presentation: unknown, options?: Pick<OrganizationLogoOptions, "organization">): OrganizationLogoState | null;
130
+ export interface LogoChoiceState {
131
+ /** "primary" (unset), "none" (false), "reference" (a logo reference) or "custom" (any other value). */
132
+ mode: "primary" | "none" | "reference" | "custom";
133
+ organization?: string;
134
+ shape?: LogoShape;
135
+ value: unknown;
136
+ scope: "slide" | "deck" | "default";
137
+ inherited: boolean;
138
+ }
139
+ /** The `design.logo` choice at a scope. */
140
+ export declare function readLogoChoice(presentation: unknown, options?: Pick<DesignOptionOptions, "slideIndex">): LogoChoiceState;
94
141
  export interface HeaderFooterZoneFields {
95
- logo?: boolean | null;
96
142
  text?: string | null;
97
143
  image?: string | Record<string, unknown> | null;
98
144
  date?: boolean | string | null;
@@ -113,11 +159,21 @@ export interface ZoneValueOptions extends DesignOptionOptions {
113
159
  export declare function prepareZoneValue(presentation: unknown, which: "header" | "footer", zone: HeaderFooterZone, name: ZoneValue["name"], options?: ZoneValueOptions): PreparedDesignOption;
114
160
  /** Insert a value's token into one zone's `text` as a single undoable transaction. */
115
161
  export declare function insertZoneValue(editor: EditorSession, which: "header" | "footer", zone: HeaderFooterZone, name: ZoneValue["name"], options?: ZoneValueOptions): DesignOptionChange;
162
+ export interface ZoneLogoOptions extends DesignOptionOptions {
163
+ /** The shape to show (default `icon`). */
164
+ shape?: LogoShape;
165
+ /** The organization's id (default: the primary organization). */
166
+ organization?: string;
167
+ }
168
+ /** The patch that makes a zone show an organization's logo: its `image` becomes `var:organization.logo.<shape>` (`var:organization.<id>.logo.<shape>` for a named organization). */
169
+ export declare function prepareZoneLogo(presentation: unknown, which: "header" | "footer", zone: HeaderFooterZone, options?: ZoneLogoOptions): PreparedDesignOption;
170
+ /** Make a zone show an organization's logo as a single undoable transaction. */
171
+ export declare function insertZoneLogo(editor: EditorSession, which: "header" | "footer", zone: HeaderFooterZone, options?: ZoneLogoOptions): DesignOptionChange;
116
172
  /** One zone's fields as they apply at a scope: the slide's own header or footer when it has one, else the deck's. */
117
173
  export declare function readHeaderFooterZone(presentation: unknown, which: "header" | "footer", zone: HeaderFooterZone, options?: Pick<DesignOptionOptions, "slideIndex">): HeaderFooterZoneFields;
118
174
  /** Whether the scope sets the header or footer itself (`own`), shows the deck's (`inherited`) or hides it with `false` (`hidden`). */
119
175
  export declare function headerFooterState(presentation: unknown, which: "header" | "footer", options?: Pick<DesignOptionOptions, "slideIndex">): { own: boolean; inherited: boolean; hidden: boolean };
120
- /** Whether a logo resolves for the slide: slide design, deck design, then the primary organization. */
121
- export declare function hasResolvableLogo(presentation: unknown, slideIndex: number): boolean;
122
- /** Settings that need content the document does not have: a logo (zones with `logo: true`, picture bullets), the organization's social profiles, or a value a zone's `text` asks for (`{{organization.name}}` without an organization, `{{slide.section}}` on a slide without a section). */
176
+ /** Whether a logo resolves for the slide: its `design.logo`, then the deck's, then the primary organization's (`shape`: the shape asked for, default full). */
177
+ export declare function hasResolvableLogo(presentation: unknown, slideIndex: number, options?: { shape?: LogoShape }): boolean;
178
+ /** Settings that need content the document does not have: a logo (a zone image or `design.logo` that names a logo no organization has, picture bullets), the organization's social profiles, or a value a zone's `text` asks for (`{{organization.name}}` without an organization, `{{slide.section}}` on a slide without a section). */
123
179
  export declare function designWarnings(presentation: unknown, slideIndex?: number): DesignWarning[];