@memorysmithapp/markdown-renderer 0.7.1 → 0.8.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.
package/dist/edit/live.js CHANGED
@@ -14,7 +14,7 @@ import { createRoot } from 'react-dom/client';
14
14
  import { followable } from '../address.js';
15
15
  import { highlightTree, languageToLoad, loadGrammar } from '../highlight.js';
16
16
  import { HostProvider, resolveHost } from '../host.js';
17
- import { readImageAlt } from '../image-dimensions.js';
17
+ import { MarkdownImage } from '../Markdown.js';
18
18
  import { highlightLanguage } from '../highlight.js';
19
19
  import { analyze } from './analyze.js';
20
20
  import { DEFAULT_METRICS, estimateHeight, lineHeightOf, measureLines, measuredHeight, rememberHeight, withinReason } from './heights.js';
@@ -256,7 +256,9 @@ class BlockWidget extends WidgetType {
256
256
  mount(view, element, this.draw(view), this.source, this.identity, measured, held);
257
257
  element.addEventListener('mousedown', (event) => {
258
258
  const target = event.target;
259
- if (target.closest('[data-ms-control]'))
259
+ // A control's, or a press in a frame its controls hold busy — a field focused, a handle
260
+ // dragged, the click a touch on them leaves behind — never shows the source (#107).
261
+ if (target.closest('[data-ms-control], [data-ms-busy]'))
260
262
  return;
261
263
  if (event.metaKey || event.ctrlKey)
262
264
  return;
@@ -425,15 +427,22 @@ class MathWidget extends WidgetType {
425
427
  return false;
426
428
  }
427
429
  }
428
- /** A Markdown image's description and address, when its address is one the page follows. */
430
+ /**
431
+ * A Markdown image's alt text, address and title, when its address is one the page follows: a
432
+ * bare destination, or one in `<>` that may hold spaces, `<assets/diagram final.png>`.
433
+ */
429
434
  function readImage(source) {
430
- const match = /^!\[([^\]]*)\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)$/.exec(source);
431
- const address = match ? followable(match[2] ?? '') : '';
432
- return match && address ? { alt: readImageAlt(match[1] ?? ''), address } : null;
435
+ const match = /^!\[((?:\\.|[^\]\\])*)\]\(\s*(?:<([^>\n]*)>|([^\s)]+))(?:\s+(?:"([^"]*)"|'([^']*)'))?\s*\)$/.exec(source);
436
+ const address = match ? followable(match[2] ?? match[3] ?? '') : '';
437
+ return match && address ? { alt: match[1] ?? '', address, title: match[4] ?? match[5] } : null;
433
438
  }
434
- /** An image drawn with its description and the dimensions it declares, as the page draws it (`SPEC.md` §3.14). */
435
- function drawImage({ alt, address }) {
436
- return createElement('img', { className: 'cm-ms-image', src: address, alt: alt.description, width: alt.width ?? undefined, height: alt.height ?? undefined });
439
+ /**
440
+ * An image drawn as the page draws it, by the page's own component: with its description and the
441
+ * dimensions it declares (`SPEC.md` §3.14), and as the kept file it names when its address names
442
+ * no host (§5.8, #104), so edit mode asks nothing of a path under the page either.
443
+ */
444
+ function drawImage({ alt, address, title }) {
445
+ return createElement(MarkdownImage, { className: 'cm-ms-image', src: address, alt, ...(title === undefined ? {} : { title }) });
437
446
  }
438
447
  // ── Decorations ──────────────────────────────────────────────────────────────
439
448
  const hide = Decoration.replace({});
package/dist/host.d.ts CHANGED
@@ -93,7 +93,7 @@ export interface NoteHost {
93
93
  readonly fileOf?: (name: string) => KeptFile | null | PromiseLike<KeptFile | null>;
94
94
  /**
95
95
  * Draws a file the notebook keeps where a note embeds it: an image, audio, video, or a card. The
96
- * dimensions are the ones the embed declared, in CSS pixels, or `null`.
96
+ * dimensions are the ones the embed declared, in CSS pixels or as a share of the page, or `null`.
97
97
  *
98
98
  * In edit mode the node may be drawn outside the host's React tree, in the root of a block the
99
99
  * editor draws, so it must not depend on the host's context; a node that throws there leaves the
@@ -199,8 +199,22 @@ export interface AttachAttributes {
199
199
  }
200
200
  export interface AttachmentRequest {
201
201
  readonly name: string;
202
+ /** The width and height the embed declared, in CSS pixels, or `null`. */
202
203
  readonly width: number | null;
203
204
  readonly height: number | null;
205
+ /**
206
+ * The width it declared as a share of the width the note is read at, from 1 to 100, which it is
207
+ * drawn at most, or `null` (#108). `width` and `height` are then `null`.
208
+ */
209
+ readonly percent: number | null;
210
+ /**
211
+ * What a Markdown image that names the file wrote before its pipe, `![description](name)`
212
+ * (`SPEC.md` §5.8, #104): an image's `alt`, a card's text. Absent for an embed, and empty when
213
+ * the image wrote none, when the file's own description is the one to draw.
214
+ */
215
+ readonly description?: string;
216
+ /** The title that image wrote after its destination, `![…](name "title")`, when it wrote one. */
217
+ readonly title?: string;
204
218
  }
205
219
  /** A link to draw through the host's router. */
206
220
  export interface LinkRequest {
@@ -10,14 +10,26 @@
10
10
  * **A value that is neither form is not a dimension.** `![Engelbart|large](…)`
11
11
  * keeps `Engelbart|large` as the whole description, because deleting what an
12
12
  * author wrote into an accessibility label is the worse of the two failures.
13
+ * A width may be a share of the page, `![Engelbart|50%](…)`, which it is drawn
14
+ * at most (#108).
15
+ *
13
16
  * Until the specification declared this form, an imported note displayed the
14
17
  * pipe and the digits on screen and mandated the whole string as the label.
15
18
  */
16
- export interface ImageAlt {
19
+ export interface ImageAlt extends Dimensions {
17
20
  /** What a reader hears, and sees when the image does not load. */
18
21
  readonly description: string;
22
+ }
23
+ /**
24
+ * What the pipe declares: a width and maybe a height in CSS pixels, or a width as a share of the
25
+ * page, at most (#108). `percent` and `width` are never both set, and a height goes only with a
26
+ * width in pixels.
27
+ */
28
+ export interface Dimensions {
19
29
  readonly width: number | null;
20
30
  readonly height: number | null;
31
+ /** The width as a share of the width the note is read at, from 1 to 100, which it is drawn at most. */
32
+ readonly percent: number | null;
21
33
  }
22
34
  /**
23
35
  * What follows a pipe, when it is a dimension and not text.
@@ -26,8 +38,21 @@ export interface ImageAlt {
26
38
  * Markdown image does — `![[engelbart.jpg|100x145]]` and
27
39
  * `![Engelbart|100x145](…)` mean one thing — so both read it here.
28
40
  */
29
- export declare function readDimensions(value: string): {
30
- width: number;
31
- height: number | null;
32
- } | null;
41
+ export declare function readDimensions(value: string): Dimensions | null;
42
+ /** The dimensions as the notation writes them after the pipe: `300`, `300x180` or `50%`; empty for none. */
43
+ export declare function writeDimensions({ width, height, percent }: Dimensions): string;
44
+ /**
45
+ * The attributes an element is drawn at for its dimensions: pixels as `width` and `height`, which
46
+ * reserve the box before the bytes arrive, a height as a style too, since the surface gives an
47
+ * image `height: auto`, which two numbers overrule; and a share of the page as a `max-width`, so
48
+ * an image narrower than it keeps its own width and is never blurred (§3.14).
49
+ */
50
+ export declare function sizedAttributes({ width, height, percent }: Dimensions): {
51
+ width?: number;
52
+ height?: number;
53
+ style?: {
54
+ height?: string;
55
+ maxWidth?: string;
56
+ };
57
+ };
33
58
  export declare function readImageAlt(alt: string): ImageAlt;
@@ -10,11 +10,16 @@
10
10
  * **A value that is neither form is not a dimension.** `![Engelbart|large](…)`
11
11
  * keeps `Engelbart|large` as the whole description, because deleting what an
12
12
  * author wrote into an accessibility label is the worse of the two failures.
13
+ * A width may be a share of the page, `![Engelbart|50%](…)`, which it is drawn
14
+ * at most (#108).
15
+ *
13
16
  * Until the specification declared this form, an imported note displayed the
14
17
  * pipe and the digits on screen and mandated the whole string as the label.
15
18
  */
16
19
  /** `100` or `100x145`, and nothing else. */
17
20
  const DIMENSIONS = /^(\d{1,5})(?:x(\d{1,5}))?$/;
21
+ /** `1%` to `100%`, digits and nothing else: no height, no mixed pair, no fraction, no space (§3.14). */
22
+ const PERCENT = /^(\d{1,3})%$/;
18
23
  /**
19
24
  * What follows a pipe, when it is a dimension and not text.
20
25
  *
@@ -23,20 +28,50 @@ const DIMENSIONS = /^(\d{1,5})(?:x(\d{1,5}))?$/;
23
28
  * `![Engelbart|100x145](…)` mean one thing — so both read it here.
24
29
  */
25
30
  export function readDimensions(value) {
26
- const measured = DIMENSIONS.exec(value.trim());
31
+ const trimmed = value.trim();
32
+ const share = PERCENT.exec(trimmed);
33
+ if (share) {
34
+ const percent = Number(share[1]);
35
+ return percent >= 1 && percent <= 100 ? { width: null, height: null, percent } : null;
36
+ }
37
+ const measured = DIMENSIONS.exec(trimmed);
27
38
  if (!measured)
28
39
  return null;
29
40
  return {
30
41
  width: Number(measured[1]),
31
42
  height: measured[2] === undefined ? null : Number(measured[2]),
43
+ percent: null,
44
+ };
45
+ }
46
+ /** The dimensions as the notation writes them after the pipe: `300`, `300x180` or `50%`; empty for none. */
47
+ export function writeDimensions({ width, height, percent }) {
48
+ if (percent !== null)
49
+ return `${percent}%`;
50
+ if (width === null)
51
+ return '';
52
+ return height === null ? String(width) : `${width}x${height}`;
53
+ }
54
+ /**
55
+ * The attributes an element is drawn at for its dimensions: pixels as `width` and `height`, which
56
+ * reserve the box before the bytes arrive, a height as a style too, since the surface gives an
57
+ * image `height: auto`, which two numbers overrule; and a share of the page as a `max-width`, so
58
+ * an image narrower than it keeps its own width and is never blurred (§3.14).
59
+ */
60
+ export function sizedAttributes({ width, height, percent }) {
61
+ if (percent !== null)
62
+ return { style: { maxWidth: `${percent}%` } };
63
+ return {
64
+ ...(width === null ? {} : { width }),
65
+ ...(height === null ? {} : { height, style: { height: `${height}px` } }),
32
66
  };
33
67
  }
68
+ const UNSIZED = { width: null, height: null, percent: null };
34
69
  export function readImageAlt(alt) {
35
70
  const at = alt.lastIndexOf('|');
36
71
  if (at === -1)
37
- return { description: alt, width: null, height: null };
72
+ return { description: alt, ...UNSIZED };
38
73
  const measured = readDimensions(alt.slice(at + 1));
39
74
  if (!measured)
40
- return { description: alt, width: null, height: null };
41
- return { description: alt.slice(0, at).trim(), width: measured.width, height: measured.height };
75
+ return { description: alt, ...UNSIZED };
76
+ return { description: alt.slice(0, at).trim(), ...measured };
42
77
  }
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export type { AttachAttributes, AttachmentRequest, AttachProgress, AttachRefusal
6
6
  export { ENGLISH, type EditorWords, type NoteStrings, type NoteStringsInput } from './strings.js';
7
7
  export { customPropertyOf, DEFAULT_THEME, defineTheme, THEME_PROPERTIES, THEME_SLOTS, type BlockquoteSlotProps, type CalloutSlotProps, type CodeBlockSlotProps, type DiagramSlotProps, type EmbedSlotProps, type FootnotesSlotProps, type HeadingSlotProps, type InlineCodeSlotProps, type ModeSwitchSlotProps, type AttachmentSlotProps, type PropertiesSlotProps, type Scheme, type TableSlotProps, type TaskBoxSlotProps, type Theme, type ThemeComponents, type ThemeProperties, type WikilinkSlotProps, } from './theme.js';
8
8
  export { followable } from './address.js';
9
- export { readDimensions, readImageAlt, type ImageAlt } from './image-dimensions.js';
9
+ export { readDimensions, readImageAlt, writeDimensions, type Dimensions, type ImageAlt } from './image-dimensions.js';
10
10
  export { codeRegions, fenceRegions, headingKey, remoteImageHosts, resolveWikilinks, splitFrontmatter, tableRegions, toUnixNewlines, type SplitDocument, } from './markdown.js';
11
11
  export { ordinalAt, taskBoxes, taskCheckedAt, toggleTaskAt } from './tasklist.js';
12
12
  export { readProperties, type NoteProperties, type PropertyEntry, type PropertyShape } from './properties.js';
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { isAttachRefusal } from './host.js';
4
4
  export { ENGLISH } from './strings.js';
5
5
  export { customPropertyOf, DEFAULT_THEME, defineTheme, THEME_PROPERTIES, THEME_SLOTS, } from './theme.js';
6
6
  export { followable } from './address.js';
7
- export { readDimensions, readImageAlt } from './image-dimensions.js';
7
+ export { readDimensions, readImageAlt, writeDimensions } from './image-dimensions.js';
8
8
  export { codeRegions, fenceRegions, headingKey, remoteImageHosts, resolveWikilinks, splitFrontmatter, tableRegions, toUnixNewlines, } from './markdown.js';
9
9
  export { ordinalAt, taskBoxes, taskCheckedAt, toggleTaskAt } from './tasklist.js';
10
10
  export { readProperties } from './properties.js';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * An image whose destination names no host is a file the notebook keeps (`SPEC.md` §5.8, #104):
3
+ * `![Curva|300](../assets/curva-2026.png)` is the kept file `curva-2026.png`, drawn at 300 pixels,
4
+ * and never a request to a path under the page. It is the form a vault exported from a desktop
5
+ * editor writes, so every imported note with pictures has it.
6
+ *
7
+ * The page once handed such a destination to `<img>` as it was, and the browser asked for it
8
+ * relative to the page: a 404, and nothing drawn, while `![[curva-2026.png]]` beside it drew the file.
9
+ */
10
+ /**
11
+ * The name of the kept file an image's destination names, or `null` when it names a host or has a
12
+ * scheme — the remote image of §3.14 and §7.10, or an address the page refuses.
13
+ *
14
+ * The name is the last segment of the path, `/` and `\` both separating, after the query and the
15
+ * fragment are dropped, percent-decoded — an escape that is not one stays as written — and in NFC.
16
+ * Nothing else is folded: the comparison is case-exact, and no extension is removed, because an
17
+ * attachment's extension is part of its name. The destination may arrive as the author wrote it or
18
+ * as the parser normalised it, with its spaces and backslashes encoded; both name the same file.
19
+ */
20
+ export declare function keptImageName(destination: string): string | null;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * An image whose destination names no host is a file the notebook keeps (`SPEC.md` §5.8, #104):
3
+ * `![Curva|300](../assets/curva-2026.png)` is the kept file `curva-2026.png`, drawn at 300 pixels,
4
+ * and never a request to a path under the page. It is the form a vault exported from a desktop
5
+ * editor writes, so every imported note with pictures has it.
6
+ *
7
+ * The page once handed such a destination to `<img>` as it was, and the browser asked for it
8
+ * relative to the page: a 404, and nothing drawn, while `![[curva-2026.png]]` beside it drew the file.
9
+ */
10
+ /** Anything of the shape `word:`, which is what makes an address absolute (§5.2, step 1). */
11
+ const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
12
+ /**
13
+ * The name of the kept file an image's destination names, or `null` when it names a host or has a
14
+ * scheme — the remote image of §3.14 and §7.10, or an address the page refuses.
15
+ *
16
+ * The name is the last segment of the path, `/` and `\` both separating, after the query and the
17
+ * fragment are dropped, percent-decoded — an escape that is not one stays as written — and in NFC.
18
+ * Nothing else is folded: the comparison is case-exact, and no extension is removed, because an
19
+ * attachment's extension is part of its name. The destination may arrive as the author wrote it or
20
+ * as the parser normalised it, with its spaces and backslashes encoded; both name the same file.
21
+ */
22
+ export function keptImageName(destination) {
23
+ const trimmed = destination.trim();
24
+ if (trimmed === '' || HAS_SCHEME.test(trimmed) || trimmed.startsWith('//'))
25
+ return null;
26
+ const path = trimmed.split(/[?#]/, 1)[0] ?? '';
27
+ // A backslash the parser encoded separates segments as one written does.
28
+ const segments = path.replace(/%5c/gi, '\\').split(/[/\\]/);
29
+ return decodeLeniently(segments[segments.length - 1] ?? '').normalize('NFC');
30
+ }
31
+ /** Percent-decodes every escape that is one, and leaves the rest as written (§5.2, step 5). */
32
+ function decodeLeniently(text) {
33
+ try {
34
+ return decodeURIComponent(text);
35
+ }
36
+ catch {
37
+ // A run of escapes that is not UTF-8, or a `%` that opens nothing: each run is decoded on its own.
38
+ return text.replace(/(?:%[0-9a-f]{2})+/gi, (run) => {
39
+ try {
40
+ return decodeURIComponent(run);
41
+ }
42
+ catch {
43
+ return run;
44
+ }
45
+ });
46
+ }
47
+ }
package/dist/markdown.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // The text helpers of the reading surface: what a note becomes before the parser reads it.
2
2
  // Nothing here rewrites the note a host keeps; each function returns a new string for display.
3
- import { readDimensions } from './image-dimensions.js';
3
+ import { readDimensions, writeDimensions } from './image-dimensions.js';
4
4
  /**
5
5
  * A note is bytes the notebook wrote, and a notebook written on Windows, or in a
6
6
  * desktop editor, or pasted from anywhere, carries CRLF. Every reader in this
@@ -351,9 +351,7 @@ function resolveWikilinksIn(body, resolve, keeps) {
351
351
  // Dropping it here is what made `![[picture|120]]` render at full size:
352
352
  // the notation was read and then thrown away one line before it was used.
353
353
  const measured = label === undefined ? null : readDimensions(label);
354
- const sized = measured
355
- ? `|${measured.width}${measured.height === null ? '' : `x${measured.height}`}`
356
- : '';
354
+ const sized = measured ? `|${writeDimensions(measured)}` : '';
357
355
  return `[${text}](attachment:${encodeURIComponent(clean)}${sized})`;
358
356
  }
359
357
  const url = resolve(clean.normalize('NFC'));
@@ -5,3 +5,6 @@ export declare const ICON_COPY: import("react").JSX.Element;
5
5
  export declare const ICON_CHECK: import("react").JSX.Element;
6
6
  export declare const ICON_CLOSE: import("react").JSX.Element;
7
7
  export declare const ICON_OWN_SIZE: import("react").JSX.Element;
8
+ export declare const ICON_RATIO_LOCKED: import("react").JSX.Element;
9
+ export declare const ICON_RATIO_FREE: import("react").JSX.Element;
10
+ export declare const ICON_RESIZE: import("react").JSX.Element;
@@ -9,5 +9,10 @@ export const ICON_MARKDOWN = svg('M10 12.5 8 15l2 2.5', 'm14 12.5 2 2.5-2 2.5',
9
9
  export const ICON_COPY = svg('M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2z', 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2');
10
10
  export const ICON_CHECK = svg('M20 6 9 17l-5-5');
11
11
  export const ICON_CLOSE = svg('M18 6 6 18', 'm6 6 12 12');
12
- // The button that draws an image at its own size (#98): Lucide's scaling (ISC).
13
- export const ICON_OWN_SIZE = svg('M21 3 9 15', 'M12 3H3v18h18v-9', 'M16 3h5v5', 'M14 15H9v-5');
12
+ // The image's controls (#106), Lucide's (ISC): its own size again, rotate-ccw, an undo of the
13
+ // size and not a way out of the page; the lock between width and height, link-2 and unlink-2;
14
+ // and the handle's diagonal arrows, move-diagonal-2.
15
+ export const ICON_OWN_SIZE = svg('M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', 'M3 3v5h5');
16
+ export const ICON_RATIO_LOCKED = svg('M9 17H7A5 5 0 0 1 7 7h2', 'M15 7h2a5 5 0 1 1 0 10h-2', 'M8 12h8');
17
+ export const ICON_RATIO_FREE = svg('M9 17H7A5 5 0 0 1 7 7', 'M15 7h2a5 5 0 0 1 4 8', 'M8 12h1', 'M2 2l20 20');
18
+ export const ICON_RESIZE = svg('M19 13v6h-6', 'M5 11V5h6', 'm5 5 14 14');
package/dist/strings.d.ts CHANGED
@@ -158,12 +158,27 @@ export interface EditorWords {
158
158
  readonly addItem: string;
159
159
  readonly removeItem: string;
160
160
  readonly calloutType: string;
161
- /** The field of an image's width in edit mode, in pixels (#98). */
161
+ /** The field of an image's width in edit mode, in the unit beside it (#98, #106). */
162
162
  readonly imageWidth: string;
163
+ /** The field of an image's height in edit mode, in pixels (#106). */
164
+ readonly imageHeight: string;
165
+ /** The choice of the width's unit: pixels, or a share of the page (#108). */
166
+ readonly imageUnit: string;
167
+ /** The field of a Markdown image's description, the text before its pipe (#106). */
168
+ readonly imageDescription: string;
169
+ /** The lock between the width and the height, locked: the height follows the width (#106). */
170
+ readonly imageKeepRatio: string;
171
+ /** The same lock, open: the width and the height are written apart (#106). */
172
+ readonly imageFreeRatio: string;
163
173
  /** The button that draws an image at its own size again (#98). */
164
174
  readonly imageOwnSize: string;
165
175
  /** The handle at an image's corner that drags its size (#98). */
166
176
  readonly imageResize: string;
177
+ /** What the handle does while the proportion is kept, and while it is not (#106). */
178
+ readonly imageResizeKeep: string;
179
+ readonly imageResizeFree: string;
180
+ /** Why a size typed was not written: the range it has to be in, in the unit of its field (#105). */
181
+ readonly imageOutOfRange: (min: number, max: number, unit: string) => string;
167
182
  readonly fold: string;
168
183
  readonly foldNone: string;
169
184
  readonly foldOpen: string;
package/dist/strings.js CHANGED
@@ -156,9 +156,17 @@ export const ENGLISH = {
156
156
  addItem: 'Add',
157
157
  removeItem: 'Remove',
158
158
  calloutType: 'Type',
159
- imageWidth: 'Width, in pixels',
159
+ imageWidth: 'Width',
160
+ imageHeight: 'Height',
161
+ imageUnit: 'Unit',
162
+ imageDescription: 'Description',
163
+ imageKeepRatio: 'Keep the proportion',
164
+ imageFreeRatio: 'Width and height apart',
160
165
  imageOwnSize: 'Its own size',
161
166
  imageResize: 'Drag to resize',
167
+ imageResizeKeep: 'Drag to resize (keeps the proportion)',
168
+ imageResizeFree: 'Drag to resize (free)',
169
+ imageOutOfRange: (min, max, unit) => `Between ${min} and ${max} ${unit}`,
162
170
  fold: 'Folding',
163
171
  foldNone: 'Not foldable',
164
172
  foldOpen: 'Folds, open',
package/dist/theme.d.ts CHANGED
@@ -268,7 +268,10 @@ export interface ModeSwitchSlotProps {
268
268
  export interface AttachmentSlotProps {
269
269
  /** The name it is kept under, as the embed wrote it. */
270
270
  readonly name: string;
271
- /** What the host said of it, its addresses already through the package's filter. */
271
+ /**
272
+ * What the host said of it, its addresses already through the package's filter, and its
273
+ * description the one a Markdown image that names it wrote, when it wrote one (#104).
274
+ */
272
275
  readonly file: KeptFile;
273
276
  /** The shape its media type is drawn in: `image`, `audio`, `video` or `card`. */
274
277
  readonly shape: FileShape;
@@ -281,6 +284,10 @@ export interface AttachmentSlotProps {
281
284
  /** The dimensions the embed declared (§3.14), in CSS pixels, or `null`. */
282
285
  readonly width: number | null;
283
286
  readonly height: number | null;
287
+ /** The width it declared as a share of the page, from 1 to 100, which it is drawn at most, or `null` (#108). */
288
+ readonly percent: number | null;
289
+ /** The title a Markdown image that names it wrote after its destination, or `null` (#104). */
290
+ readonly title: string | null;
284
291
  }
285
292
  /** The slots a theme may fill. Each one left out is drawn as the default theme draws it. */
286
293
  export interface ThemeComponents {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorysmithapp/markdown-renderer",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "A React component that draws a MemorySmith note exactly as MemorySmith's reading page does.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -79,7 +79,7 @@
79
79
  "unified": "11.0.5"
80
80
  },
81
81
  "devDependencies": {
82
- "@memorysmithapp/markdown-spec": "0.7.1",
82
+ "@memorysmithapp/markdown-spec": "0.8.0",
83
83
  "@types/hast": "3.0.5",
84
84
  "@types/react": "19.2.18",
85
85
  "@types/react-dom": "19.2.4",
package/structure.css CHANGED
@@ -459,8 +459,10 @@
459
459
  max-width: 100%;
460
460
  vertical-align: middle;
461
461
  }
462
- /* An image in edit mode carries its size's controls over its top right and a
463
- handle at its bottom right corner that drags it (#98). */
462
+ /* An image in edit mode carries its controls under it, never over the picture,
463
+ whose top left is often its subject, and a handle at its bottom right corner
464
+ that drags it (#98, #106). The controls hang from the image's bottom edge with
465
+ no gap, so the pointer travels from the picture to them without leaving it. */
464
466
  .markdown .edit-image {
465
467
  position: relative;
466
468
  display: inline-block;
@@ -468,19 +470,55 @@
468
470
  vertical-align: middle;
469
471
  }
470
472
  .markdown .edit-image-controls {
473
+ position: absolute;
474
+ top: var(--edit-image-bottom, 100%);
475
+ left: 0;
476
+ z-index: 2;
477
+ display: flex;
478
+ flex-wrap: wrap;
479
+ align-items: center;
480
+ gap: 0.25rem;
481
+ width: max-content;
482
+ min-width: 100%;
483
+ max-width: min(26rem, calc(100vw - 2rem));
484
+ box-sizing: border-box;
485
+ }
486
+ .markdown .edit-image-size {
487
+ display: inline-flex;
471
488
  align-items: center;
489
+ gap: 0.15rem;
472
490
  white-space: nowrap;
473
491
  }
474
- .markdown .edit-image-width {
475
- width: 5em;
492
+ .markdown .edit-image-width,
493
+ .markdown .edit-image-height {
494
+ width: 4.2em;
476
495
  }
496
+ .markdown .edit-image-description {
497
+ flex: 1 1 10rem;
498
+ min-width: 0;
499
+ }
500
+ .markdown .edit-image-refused {
501
+ flex-basis: 100%;
502
+ }
503
+ /* The handle is a touch target of at least 24px at the picture's corner, whose arrows say it resizes. */
477
504
  .markdown .edit-image-handle {
478
505
  position: absolute;
479
- right: 0;
480
- bottom: 0;
506
+ top: calc(var(--edit-image-bottom, 100%) - 24px);
507
+ left: calc(var(--edit-image-right, 100%) - 24px);
481
508
  z-index: 1;
482
- width: 0.9rem;
483
- height: 0.9rem;
509
+ display: inline-flex;
510
+ align-items: center;
511
+ justify-content: center;
512
+ width: 24px;
513
+ height: 24px;
484
514
  cursor: nwse-resize;
485
515
  touch-action: none;
486
516
  }
517
+ /* Where nothing hovers, the controls stand in the flow under the image, so
518
+ they never lie over the text that follows it. */
519
+ @media (hover: none) {
520
+ .markdown .edit-image-controls {
521
+ position: static;
522
+ min-width: 0;
523
+ }
524
+ }
package/theme-default.css CHANGED
@@ -1950,38 +1950,79 @@
1950
1950
  opacity: 1;
1951
1951
  }
1952
1952
  .markdown .edit-image-controls {
1953
+ opacity: 0;
1954
+ padding: 0.2rem;
1955
+ border: 1px solid var(--ms-border);
1956
+ border-radius: var(--ms-radius);
1957
+ background: var(--ms-surface);
1953
1958
  font-size: 0.8rem;
1954
1959
  line-height: 1.2;
1960
+ transition: opacity 0.12s;
1961
+ pointer-events: none;
1955
1962
  }
1956
- .markdown .edit-image-width.edit-field {
1963
+ .markdown .edit-image:hover .edit-image-controls,
1964
+ .markdown .edit-image:focus-within .edit-image-controls,
1965
+ .markdown .edit-image[data-ms-busy] .edit-image-controls,
1966
+ .markdown .edit-image[data-resizing] .edit-image-controls {
1967
+ opacity: 1;
1968
+ pointer-events: auto;
1969
+ }
1970
+ .markdown .edit-image-controls .edit-field {
1957
1971
  height: 1.9rem;
1958
1972
  border-color: var(--ms-border);
1959
1973
  background: var(--ms-surface);
1960
1974
  color: var(--ms-text);
1961
1975
  font-size: 0.8rem;
1962
1976
  }
1977
+ .markdown .edit-image-controls .edit-image-width,
1978
+ .markdown .edit-image-controls .edit-image-height {
1979
+ flex: none;
1980
+ width: 4.2em;
1981
+ }
1982
+ .markdown .edit-image-controls .edit-field[aria-invalid='true'] {
1983
+ border-color: rgb(var(--ms-callout-stop));
1984
+ outline: 1px solid rgb(var(--ms-callout-stop));
1985
+ }
1986
+ .markdown .edit-image-controls .edit-field:disabled {
1987
+ opacity: 0.5;
1988
+ }
1963
1989
  .markdown .edit-image-unit {
1964
- padding-inline-end: 0.2rem;
1990
+ height: 1.9rem;
1991
+ border: 1px solid var(--ms-border);
1992
+ border-radius: var(--ms-radius);
1993
+ background: var(--ms-surface);
1994
+ color: var(--ms-text-soft);
1995
+ font-size: 0.8rem;
1996
+ }
1997
+ .markdown .edit-image-unit-label {
1998
+ padding-inline: 0.1rem 0.2rem;
1965
1999
  color: var(--ms-text-soft);
1966
2000
  font-size: 0.8rem;
1967
2001
  }
1968
- /* The handle is a corner of the accent's colour, shown with the controls. */
2002
+ .markdown .edit-image-refused {
2003
+ color: rgb(var(--ms-callout-stop));
2004
+ font-size: 0.75rem;
2005
+ }
2006
+ /* The handle is the accent's diagonal arrows on the surface, shown with the controls. */
1969
2007
  .markdown .edit-image-handle {
1970
2008
  opacity: 0;
1971
- border-right: 3px solid var(--ms-accent);
1972
- border-bottom: 3px solid var(--ms-accent);
1973
- border-bottom-right-radius: 3px;
2009
+ border-top-left-radius: var(--ms-radius);
2010
+ background: var(--ms-surface);
2011
+ color: var(--ms-accent);
2012
+ font-size: 16px;
1974
2013
  transition: opacity 0.12s;
1975
2014
  }
1976
2015
  .markdown .edit-image:hover .edit-image-handle,
2016
+ .markdown .edit-image[data-ms-busy] .edit-image-handle,
1977
2017
  .markdown .edit-image[data-resizing] .edit-image-handle {
1978
2018
  opacity: 1;
1979
2019
  }
1980
2020
  /* Where nothing hovers, as on a phone, an image's size is always at hand. */
1981
2021
  @media (hover: none) {
1982
- .markdown .edit-image .edit-block-controls,
2022
+ .markdown .edit-image .edit-image-controls,
1983
2023
  .markdown .edit-image .edit-image-handle {
1984
2024
  opacity: 1;
2025
+ pointer-events: auto;
1985
2026
  }
1986
2027
  }
1987
2028
  .markdown .edit-image[data-resizing] .cm-ms-image,