@happyvertical/smrt-content 0.51.38 → 0.52.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.
Files changed (72) hide show
  1. package/AGENTS.md +6 -0
  2. package/README.md +76 -0
  3. package/dist/body-format.d.ts +104 -0
  4. package/dist/body-format.d.ts.map +1 -1
  5. package/dist/body-format.js +439 -48
  6. package/dist/body-format.js.map +1 -1
  7. package/dist/chunks/content-query-BqLHbZYv.js +4131 -0
  8. package/dist/chunks/content-query-BqLHbZYv.js.map +1 -0
  9. package/dist/chunks/{content-versions-CiA4oUsl.js → content-versions-DatN0G28.js} +2 -2
  10. package/dist/chunks/{content-versions-CiA4oUsl.js.map → content-versions-DatN0G28.js.map} +1 -1
  11. package/dist/chunks/{contents-BlHrLlpl.js → contents-xas83N8M.js} +4 -6
  12. package/dist/chunks/contents-xas83N8M.js.map +1 -0
  13. package/dist/content-query.d.ts.map +1 -1
  14. package/dist/content.d.ts +13 -1
  15. package/dist/content.d.ts.map +1 -1
  16. package/dist/contents.d.ts.map +1 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -6
  20. package/dist/manifest.json +113 -23
  21. package/dist/server/content-list-data-surface.d.ts +1 -1
  22. package/dist/server/content-list-data-surface.d.ts.map +1 -1
  23. package/dist/server.js +3 -3
  24. package/dist/server.js.map +1 -1
  25. package/dist/smrt-knowledge.json +92 -10
  26. package/dist/svelte/components/ContentBodyEditor.svelte +898 -38
  27. package/dist/svelte/components/ContentBodyEditor.svelte.d.ts +67 -0
  28. package/dist/svelte/components/ContentBodyEditor.svelte.d.ts.map +1 -1
  29. package/dist/svelte/components/ContentBodyRenderer.svelte +36 -11
  30. package/dist/svelte/components/ContentBodyRenderer.svelte.d.ts +2 -0
  31. package/dist/svelte/components/ContentBodyRenderer.svelte.d.ts.map +1 -1
  32. package/dist/svelte/components/ContentClaimAuditTool.svelte +45 -2
  33. package/dist/svelte/components/ContentCorrectionsTool.svelte +24 -2
  34. package/dist/svelte/components/ContentEditor.svelte +67 -17
  35. package/dist/svelte/components/ContentEditor.svelte.d.ts +12 -0
  36. package/dist/svelte/components/ContentEditor.svelte.d.ts.map +1 -1
  37. package/dist/svelte/components/ContentGovernancePanel.svelte +4 -2
  38. package/dist/svelte/components/ContentMetadataFields.svelte +141 -51
  39. package/dist/svelte/components/ContentMetadataFields.svelte.d.ts +20 -0
  40. package/dist/svelte/components/ContentMetadataFields.svelte.d.ts.map +1 -1
  41. package/dist/svelte/components/ContentPictureDrawer.svelte +611 -0
  42. package/dist/svelte/components/ContentPictureDrawer.svelte.d.ts +68 -0
  43. package/dist/svelte/components/ContentPictureDrawer.svelte.d.ts.map +1 -0
  44. package/dist/svelte/components/ContentReviewStatusTray.svelte +2 -2
  45. package/dist/svelte/components/ContentStatusFields.svelte +85 -38
  46. package/dist/svelte/components/ContentStatusFields.svelte.d.ts +10 -0
  47. package/dist/svelte/components/ContentStatusFields.svelte.d.ts.map +1 -1
  48. package/dist/svelte/components/ContentTitleField.svelte +109 -12
  49. package/dist/svelte/components/ContentTitleField.svelte.d.ts +12 -0
  50. package/dist/svelte/components/ContentTitleField.svelte.d.ts.map +1 -1
  51. package/dist/svelte/components/ContentTransparencyReport.svelte +3 -1
  52. package/dist/svelte/components/ContentTransparencyTool.svelte +18 -3
  53. package/dist/svelte/components/ContentVersionsTool.svelte +26 -2
  54. package/dist/svelte/components/content-field-mode.d.ts +20 -0
  55. package/dist/svelte/components/content-field-mode.d.ts.map +1 -0
  56. package/dist/svelte/components/content-field-mode.js +26 -0
  57. package/dist/svelte/content-editor-media.d.ts +14 -0
  58. package/dist/svelte/content-editor-media.d.ts.map +1 -1
  59. package/dist/svelte/content-editor-media.js +41 -0
  60. package/dist/svelte/content-editor-state.svelte.d.ts +17 -0
  61. package/dist/svelte/content-editor-state.svelte.d.ts.map +1 -1
  62. package/dist/svelte/content-editor-state.svelte.js +46 -0
  63. package/dist/svelte/i18n.editor.d.ts +49 -0
  64. package/dist/svelte/i18n.editor.d.ts.map +1 -1
  65. package/dist/svelte/i18n.editor.js +53 -1
  66. package/dist/svelte/index.d.ts +10 -5
  67. package/dist/svelte/index.d.ts.map +1 -1
  68. package/dist/svelte/index.js +5 -3
  69. package/package.json +27 -25
  70. package/dist/chunks/content-query-BTmxK8AQ.js +0 -4092
  71. package/dist/chunks/content-query-BTmxK8AQ.js.map +0 -1
  72. package/dist/chunks/contents-BlHrLlpl.js.map +0 -1
@@ -1,7 +1,14 @@
1
1
  <script lang="ts">
2
- import { Input, Select } from '@happyvertical/smrt-ui/forms';
2
+ import { FormGroup, Input, Select } from '@happyvertical/smrt-ui/forms';
3
3
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
4
4
  import { M } from '../i18n.editor.js';
5
+ import {
6
+ type ContentFieldMode,
7
+ type ContentStatusFieldName,
8
+ FULL_STATUS_FIELDS,
9
+ resolveContentFields,
10
+ SIMPLE_STATUS_FIELDS,
11
+ } from './content-field-mode.js';
5
12
 
6
13
  const { t } = useI18n();
7
14
 
@@ -16,52 +23,84 @@ export interface ContentStatusFieldData {
16
23
  export interface Props {
17
24
  /** Current status field values. */
18
25
  data: ContentStatusFieldData;
26
+ /**
27
+ * `full` (default) shows Type, State, Status and Published; `simple` shows
28
+ * Status and an optional Publish date with plain labels.
29
+ */
30
+ mode?: ContentFieldMode;
31
+ /** Explicit allow-list of fields; overrides the mode's choice. */
32
+ fields?: readonly ContentStatusFieldName[];
33
+ /** Prefix for field DOM ids, unique per editor on the page (default `content`). */
34
+ idPrefix?: string;
19
35
  /** Invoked when the user modifies any status field. */
20
36
  onChange?: (change: Record<string, unknown>) => void;
21
37
  }
22
38
 
23
- let { data, onChange = undefined }: Props = $props();
39
+ let {
40
+ data,
41
+ mode = 'full',
42
+ fields = undefined,
43
+ idPrefix = 'content',
44
+ onChange = undefined,
45
+ }: Props = $props();
46
+
47
+ const simple = $derived(mode === 'simple');
48
+ const shown = $derived(
49
+ resolveContentFields(mode, fields, SIMPLE_STATUS_FIELDS, FULL_STATUS_FIELDS),
50
+ );
51
+ const publishLabel = $derived(
52
+ simple
53
+ ? t(M['content.content_fields.optional'], {
54
+ label: t(M['content.content_fields.publish_date']),
55
+ })
56
+ : t(M['content.content_fields.published']),
57
+ );
24
58
 
25
59
  function updateField(key: string, value: unknown) {
26
60
  onChange?.({ [key]: value });
27
61
  }
28
62
  </script>
29
63
 
30
- <div class="content-status-fields">
31
- <label>
32
- <span>Type</span>
33
- <Select value={data.type || 'article'} onchange={(event) => updateField('type', event.currentTarget.value)}>
34
- <option value="article">Article</option>
35
- <option value="document">Document</option>
36
- <option value="mirror">Mirror</option>
37
- <option value="video-segment">{t(M['content.content_status_fields.video_segment'])}</option>
38
- </Select>
39
- </label>
40
- <label>
41
- <span>State</span>
42
- <Select value={data.state || 'active'} onchange={(event) => updateField('state', event.currentTarget.value)}>
43
- <option value="active">Active</option>
44
- <option value="highlighted">Highlighted</option>
45
- <option value="deprecated">Deprecated</option>
46
- </Select>
47
- </label>
48
- <label>
49
- <span>Status</span>
50
- <Select value={data.status || 'draft'} onchange={(event) => updateField('status', event.currentTarget.value)}>
51
- <option value="draft">Draft</option>
52
- <option value="review">Review</option>
53
- <option value="published">Published</option>
54
- <option value="archived">Archived</option>
55
- </Select>
56
- </label>
57
- <label>
58
- <span>Published</span>
59
- <Input
60
- type="datetime-local"
61
- value={data.publish_date || ''}
62
- onchange={(event) => updateField('publish_date', event.currentTarget.value)}
63
- />
64
- </label>
64
+ <div class="content-status-fields" class:simple>
65
+ {#if shown.has('type')}
66
+ <FormGroup label={t(M['content.content_fields.type'])} id="{idPrefix}-type">
67
+ <Select name="type" value={data.type || 'article'} onchange={(event) => updateField('type', event.currentTarget.value)}>
68
+ <option value="article">Article</option>
69
+ <option value="document">Document</option>
70
+ <option value="mirror">Mirror</option>
71
+ <option value="video-segment">{t(M['content.content_status_fields.video_segment'])}</option>
72
+ </Select>
73
+ </FormGroup>
74
+ {/if}
75
+ {#if shown.has('state')}
76
+ <FormGroup label={t(M['content.content_fields.state'])} id="{idPrefix}-state">
77
+ <Select name="state" value={data.state || 'active'} onchange={(event) => updateField('state', event.currentTarget.value)}>
78
+ <option value="active">Active</option>
79
+ <option value="highlighted">Highlighted</option>
80
+ <option value="deprecated">Deprecated</option>
81
+ </Select>
82
+ </FormGroup>
83
+ {/if}
84
+ {#if shown.has('status')}
85
+ <FormGroup label={t(M['content.content_fields.status'])} id="{idPrefix}-status">
86
+ <Select name="status" value={data.status || 'draft'} onchange={(event) => updateField('status', event.currentTarget.value)}>
87
+ <option value="draft">{t(M['content.content_fields.status_draft'])}</option>
88
+ <option value="review">{t(M['content.content_fields.status_review'])}</option>
89
+ <option value="published">{t(M['content.content_fields.status_published'])}</option>
90
+ <option value="archived">{t(M['content.content_fields.status_archived'])}</option>
91
+ </Select>
92
+ </FormGroup>
93
+ {/if}
94
+ {#if shown.has('publish_date')}
95
+ <FormGroup label={publishLabel} id="{idPrefix}-publish-date">
96
+ <Input
97
+ name="publish_date"
98
+ type="datetime-local"
99
+ value={data.publish_date || ''}
100
+ onchange={(event) => updateField('publish_date', event.currentTarget.value)}
101
+ />
102
+ </FormGroup>
103
+ {/if}
65
104
  </div>
66
105
 
67
106
  <style>
@@ -72,13 +111,21 @@ function updateField(key: string, value: unknown) {
72
111
  gap: 0.75rem;
73
112
  }
74
113
 
75
- label {
114
+ .content-status-fields :global(.form-group) {
76
115
  display: grid;
77
116
  gap: 0.35rem;
78
117
  min-width: 8.5rem;
118
+ margin: 0;
119
+ }
120
+
121
+ .content-status-fields :global(.form-label) {
122
+ margin: 0;
79
123
  color: var(--smrt-color-on-surface-variant);
80
124
  font-size: var(--smrt-typography-label-medium-size, 0.75rem);
81
125
  font-weight: var(--smrt-typography-weight-semibold, 600);
82
126
  }
83
127
 
128
+ .content-status-fields.simple :global(.form-label) {
129
+ font-size: var(--smrt-typography-label-large-size, 0.875rem);
130
+ }
84
131
  </style>
@@ -1,3 +1,4 @@
1
+ import { type ContentFieldMode, type ContentStatusFieldName } from './content-field-mode.js';
1
2
  export interface ContentStatusFieldData {
2
3
  type?: string;
3
4
  state?: string;
@@ -8,6 +9,15 @@ export interface ContentStatusFieldData {
8
9
  export interface Props {
9
10
  /** Current status field values. */
10
11
  data: ContentStatusFieldData;
12
+ /**
13
+ * `full` (default) shows Type, State, Status and Published; `simple` shows
14
+ * Status and an optional Publish date with plain labels.
15
+ */
16
+ mode?: ContentFieldMode;
17
+ /** Explicit allow-list of fields; overrides the mode's choice. */
18
+ fields?: readonly ContentStatusFieldName[];
19
+ /** Prefix for field DOM ids, unique per editor on the page (default `content`). */
20
+ idPrefix?: string;
11
21
  /** Invoked when the user modifies any status field. */
12
22
  onChange?: (change: Record<string, unknown>) => void;
13
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ContentStatusFields.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentStatusFields.svelte.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,KAAK;IACpB,mCAAmC;IACnC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACtD;AAsDD,QAAA,MAAM,mBAAmB,2CAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"ContentStatusFields.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentStatusFields.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAI5B,MAAM,yBAAyB,CAAC;AAGjC,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,KAAK;IACpB,mCAAmC;IACnC,IAAI,EAAE,sBAAsB,CAAC;IAC7B;;;OAGG;IACH,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,kEAAkE;IAClE,MAAM,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACtD;AA6ED,QAAA,MAAM,mBAAmB,2CAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
@@ -1,5 +1,19 @@
1
1
  <script lang="ts">
2
- import { Input } from '@happyvertical/smrt-ui/forms';
2
+ /**
3
+ * ContentTitleField - a content item's title as a headline-style text field.
4
+ *
5
+ * The title wraps like a heading (a one-row textarea that grows with its
6
+ * text) but stays one line of text: Enter never adds a newline, and pasted
7
+ * line breaks become spaces. It is a labelled control (`label`, default
8
+ * "Title"), so it can sit inside the page's `<h1>` (smrt-ui PageHeader's
9
+ * `titleField`): the heading is then named by the title and the field keeps
10
+ * its own label. Inside a heading it takes the heading's type.
11
+ *
12
+ * Give it a `name` (or `id`) so a surrounding agent-operable form registers
13
+ * it like any smrt-ui control. Validation messages belong outside a heading:
14
+ * render the message beside it and point `describedBy` at it.
15
+ */
16
+ import { Textarea } from '@happyvertical/smrt-ui/forms';
3
17
 
4
18
  export interface Props {
5
19
  /** Current title text value. */
@@ -8,6 +22,18 @@ export interface Props {
8
22
  placeholder?: string;
9
23
  /** Whether the title field must be filled before submission. */
10
24
  required?: boolean;
25
+ /** Accessible name of the field. */
26
+ label?: string;
27
+ /** Form field name; also its identity for agent-operable forms. */
28
+ name?: string;
29
+ /** Element id. */
30
+ id?: string;
31
+ /** Marks the field invalid (e.g. an empty required title). */
32
+ invalid?: boolean;
33
+ /** Id(s) of the element(s) describing the field, e.g. its error message. */
34
+ describedBy?: string;
35
+ /** Whether the field is disabled. */
36
+ disabled?: boolean;
11
37
  /** Invoked when the user modifies the title text. */
12
38
  onChange?: (value: string) => void;
13
39
  }
@@ -16,27 +42,80 @@ let {
16
42
  value = '',
17
43
  placeholder = 'Title',
18
44
  required = false,
45
+ label = 'Title',
46
+ name = undefined,
47
+ id = undefined,
48
+ invalid = false,
49
+ describedBy = undefined,
50
+ disabled = false,
19
51
  onChange = undefined,
20
52
  }: Props = $props();
53
+
54
+ let field = $state<ReturnType<typeof Textarea> | null>(null);
55
+
56
+ function fitHeight() {
57
+ const element = field?.getElement();
58
+ if (!element) return;
59
+ element.style.height = 'auto';
60
+ element.style.height = `${element.scrollHeight}px`;
61
+ }
62
+
63
+ $effect(() => {
64
+ void value;
65
+ fitHeight();
66
+ });
67
+
68
+ function handleKeydown(event: KeyboardEvent) {
69
+ // One line of text: Enter neither adds a newline nor submits.
70
+ if (event.key === 'Enter' && !event.isComposing) event.preventDefault();
71
+ }
72
+
73
+ function handleInput(event: Event & { currentTarget: HTMLTextAreaElement }) {
74
+ const element = event.currentTarget;
75
+ const text = element.value.replace(/[\r\n]+/g, ' ');
76
+ if (text !== element.value) {
77
+ const caret = element.selectionStart;
78
+ element.value = text;
79
+ if (caret !== null) element.setSelectionRange(caret, caret);
80
+ }
81
+ fitHeight();
82
+ onChange?.(text);
83
+ }
21
84
  </script>
22
85
 
23
- <div class="content-title-field-shell">
24
- <Input
86
+ <svelte:window onresize={fitHeight} />
87
+
88
+ <span class="content-title-field-shell">
89
+ <Textarea
90
+ bind:this={field}
25
91
  class="content-title-field"
26
- type="text"
92
+ rows={1}
93
+ {id}
94
+ {name}
27
95
  {placeholder}
28
96
  {required}
97
+ {disabled}
98
+ aria-label={label}
99
+ aria-invalid={invalid ? 'true' : undefined}
100
+ aria-describedby={describedBy}
29
101
  value={value || ''}
30
- oninput={(event) => onChange?.(event.currentTarget.value)}
102
+ onkeydown={handleKeydown}
103
+ oninput={handleInput}
31
104
  />
32
- </div>
105
+ </span>
33
106
 
34
107
  <style>
108
+ .content-title-field-shell {
109
+ display: block;
110
+ }
111
+
35
112
  .content-title-field-shell :global(.content-title-field) {
113
+ display: block;
36
114
  width: 100%;
115
+ min-height: 0;
37
116
  box-sizing: border-box;
38
117
  border: 0;
39
- border-radius: 0;
118
+ border-radius: var(--smrt-radius-sm, 4px);
40
119
  background: transparent;
41
120
  color: var(--smrt-color-on-surface);
42
121
  font: inherit;
@@ -44,19 +123,37 @@ let {
44
123
  font-weight: var(--smrt-typography-weight-bold, 720);
45
124
  line-height: 1.12;
46
125
  letter-spacing: 0;
47
- padding: 0;
126
+ padding: 0.1em 0.2em;
127
+ margin-inline: -0.2em;
128
+ resize: none;
129
+ overflow: hidden;
130
+ field-sizing: content;
131
+ transition: background-color 150ms;
132
+ }
133
+
134
+ /* Inside a heading, look like that heading. */
135
+ :global(:is(h1, h2, h3)) > .content-title-field-shell :global(.content-title-field) {
136
+ font: inherit;
137
+ letter-spacing: inherit;
48
138
  }
49
139
 
50
140
  .content-title-field-shell :global(.content-title-field)::placeholder {
51
141
  color: color-mix(in srgb, var(--smrt-color-on-surface-variant) 70%, transparent);
52
142
  }
53
143
 
54
- .content-title-field-shell :global(.content-title-field):focus {
55
- outline: none;
144
+ /* Editable on hover and focus: a quiet field background, then an outline. */
145
+ .content-title-field-shell :global(.content-title-field):hover:not(:disabled) {
146
+ background: var(--smrt-color-surface-container-low, rgb(0 0 0 / 4%));
56
147
  }
57
148
 
58
- .content-title-field-shell :global(.content-title-field):focus-visible {
149
+ .content-title-field-shell :global(.content-title-field):focus {
59
150
  outline: 2px solid var(--smrt-color-primary, #005ac1);
60
- outline-offset: 0.35rem;
151
+ outline-offset: 2px;
152
+ background: var(--smrt-color-surface-container-low, rgb(0 0 0 / 4%));
153
+ }
154
+
155
+ .content-title-field-shell :global(.content-title-field[aria-invalid='true']) {
156
+ outline: 2px solid var(--smrt-color-error, #b3261e);
157
+ outline-offset: 2px;
61
158
  }
62
159
  </style>
@@ -5,6 +5,18 @@ export interface Props {
5
5
  placeholder?: string;
6
6
  /** Whether the title field must be filled before submission. */
7
7
  required?: boolean;
8
+ /** Accessible name of the field. */
9
+ label?: string;
10
+ /** Form field name; also its identity for agent-operable forms. */
11
+ name?: string;
12
+ /** Element id. */
13
+ id?: string;
14
+ /** Marks the field invalid (e.g. an empty required title). */
15
+ invalid?: boolean;
16
+ /** Id(s) of the element(s) describing the field, e.g. its error message. */
17
+ describedBy?: string;
18
+ /** Whether the field is disabled. */
19
+ disabled?: boolean;
8
20
  /** Invoked when the user modifies the title text. */
9
21
  onChange?: (value: string) => void;
10
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ContentTitleField.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentTitleField.svelte.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,KAAK;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAqBD,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"ContentTitleField.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentTitleField.svelte.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,KAAK;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AA6DD,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -216,6 +216,8 @@ function getFactLabel(fact: {
216
216
 
217
217
  <style>
218
218
  .transparency-report {
219
+ container-type: inline-size;
220
+ container-name: transparency-report;
219
221
  display: flex;
220
222
  flex-direction: column;
221
223
  gap: 1rem;
@@ -269,7 +271,7 @@ function getFactLabel(fact: {
269
271
  gap: 1rem;
270
272
  }
271
273
 
272
- @media (min-width: 900px) {
274
+ @container transparency-report (min-width: 56rem) {
273
275
  .transparency-grid {
274
276
  grid-template-columns: repeat(2, minmax(0, 1fr));
275
277
  }
@@ -223,6 +223,13 @@ async function loadTransparency(contentIdToLoad = savedContentId) {
223
223
  </div>
224
224
 
225
225
  <style>
226
+ /* The tool lays itself out by its own width, not the viewport, so it
227
+ fits a full page, a side panel, the assistant dock or a phone. */
228
+ .governance-tool {
229
+ container-type: inline-size;
230
+ container-name: governance-tool;
231
+ }
232
+
226
233
  .governance-tool,
227
234
  .tool-list,
228
235
  .tool-list-section,
@@ -235,14 +242,15 @@ async function loadTransparency(contentIdToLoad = savedContentId) {
235
242
  .transparency-stats {
236
243
  display: grid;
237
244
  gap: 0.75rem;
238
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
245
+ grid-template-columns: repeat(auto-fit, minmax(min(120px, 100%), 1fr));
239
246
  }
240
247
 
241
248
  .transparency-stat,
242
249
  .transparency-card,
243
250
  .transparency-list-item {
244
- background: var(--smrt-color-surface);
245
- border: 1px solid var(--smrt-color-outline-variant);
251
+ background: var(--smrt-card-background, var(--smrt-color-surface));
252
+ border: var(--smrt-card-border, 1px solid var(--smrt-color-outline-variant));
253
+ box-shadow: var(--smrt-card-shadow, none);
246
254
  border-radius: 0.75rem;
247
255
  }
248
256
 
@@ -265,6 +273,7 @@ async function loadTransparency(contentIdToLoad = savedContentId) {
265
273
  }
266
274
 
267
275
  .transparency-list-item {
276
+ background: color-mix(in srgb, var(--smrt-color-on-surface) 6%, transparent);
268
277
  padding: 0.7rem 0.8rem;
269
278
  display: flex;
270
279
  flex-direction: column;
@@ -316,4 +325,10 @@ async function loadTransparency(contentIdToLoad = savedContentId) {
316
325
  color: var(--smrt-color-on-error-container);
317
326
  font-size: var(--smrt-typography-body-medium-size, 0.9rem);
318
327
  }
328
+
329
+ @container governance-tool (max-width: 30rem) {
330
+ .tool-card-header {
331
+ flex-wrap: wrap;
332
+ }
333
+ }
319
334
  </style>
@@ -241,6 +241,13 @@ async function restoreVersion(versionNumber: number) {
241
241
  />
242
242
 
243
243
  <style>
244
+ /* The tool lays itself out by its own width, not the viewport, so it
245
+ fits a full page, a side panel, the assistant dock or a phone. */
246
+ .governance-tool {
247
+ container-type: inline-size;
248
+ container-name: governance-tool;
249
+ }
250
+
244
251
  .governance-tool,
245
252
  .tool-list,
246
253
  .tool-card {
@@ -263,8 +270,9 @@ async function restoreVersion(versionNumber: number) {
263
270
  }
264
271
 
265
272
  .tool-card {
266
- background: var(--smrt-color-surface);
267
- border: 1px solid var(--smrt-color-outline-variant);
273
+ background: var(--smrt-card-background, var(--smrt-color-surface));
274
+ border: var(--smrt-card-border, 1px solid var(--smrt-color-outline-variant));
275
+ box-shadow: var(--smrt-card-shadow, none);
268
276
  border-radius: 0.75rem;
269
277
  padding: 0.85rem;
270
278
  }
@@ -309,4 +317,20 @@ async function restoreVersion(versionNumber: number) {
309
317
  background: color-mix(in srgb, var(--smrt-color-primary) 10%, transparent);
310
318
  color: var(--smrt-color-on-primary-container);
311
319
  }
320
+
321
+ @container governance-tool (max-width: 30rem) {
322
+ .tool-toolbar {
323
+ justify-content: stretch;
324
+ }
325
+
326
+ .tool-toolbar :global(button),
327
+ .tool-card-footer :global(button) {
328
+ width: 100%;
329
+ }
330
+
331
+ .tool-card-footer {
332
+ flex-direction: column;
333
+ align-items: stretch;
334
+ }
335
+ }
312
336
  </style>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Simple vs full editor fields.
3
+ *
4
+ * `full` (the default) keeps every field and the long-standing labels.
5
+ * `simple` is for everyday editors — a title, the story, images, author,
6
+ * summary and tags: it hides Type, State, URL, File key (and, in
7
+ * ContentEditor, references), collapses the Details section, uses plain
8
+ * labels, and marks optional fields "(optional)". A `fields` allow-list
9
+ * overrides either mode's choice of fields.
10
+ */
11
+ export type ContentFieldMode = 'simple' | 'full';
12
+ export type ContentStatusFieldName = 'type' | 'state' | 'status' | 'publish_date';
13
+ export type ContentMetadataFieldName = 'author' | 'description' | 'tags' | 'url' | 'fileKey';
14
+ export declare const SIMPLE_STATUS_FIELDS: readonly ContentStatusFieldName[];
15
+ export declare const FULL_STATUS_FIELDS: readonly ContentStatusFieldName[];
16
+ export declare const SIMPLE_METADATA_FIELDS: readonly ContentMetadataFieldName[];
17
+ export declare const FULL_METADATA_FIELDS: readonly ContentMetadataFieldName[];
18
+ /** The fields to render: an explicit allow-list wins over the mode. */
19
+ export declare function resolveContentFields<T extends string>(mode: ContentFieldMode, fields: readonly T[] | undefined, simple: readonly T[], full: readonly T[]): Set<T>;
20
+ //# sourceMappingURL=content-field-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-field-mode.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/content-field-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,sBAAsB,GAC9B,MAAM,GACN,OAAO,GACP,QAAQ,GACR,cAAc,CAAC;AACnB,MAAM,MAAM,wBAAwB,GAChC,QAAQ,GACR,aAAa,GACb,MAAM,GACN,KAAK,GACL,SAAS,CAAC;AAEd,eAAO,MAAM,oBAAoB,EAAE,SAAS,sBAAsB,EAGjE,CAAC;AACF,eAAO,MAAM,kBAAkB,EAAE,SAAS,sBAAsB,EAK/D,CAAC;AACF,eAAO,MAAM,sBAAsB,EAAE,SAAS,wBAAwB,EAIrE,CAAC;AACF,eAAO,MAAM,oBAAoB,EAAE,SAAS,wBAAwB,EAMnE,CAAC;AAEF,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,EACnD,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,SAAS,EAChC,MAAM,EAAE,SAAS,CAAC,EAAE,EACpB,IAAI,EAAE,SAAS,CAAC,EAAE,GACjB,GAAG,CAAC,CAAC,CAAC,CAER"}
@@ -0,0 +1,26 @@
1
+ export const SIMPLE_STATUS_FIELDS = [
2
+ 'status',
3
+ 'publish_date',
4
+ ];
5
+ export const FULL_STATUS_FIELDS = [
6
+ 'type',
7
+ 'state',
8
+ 'status',
9
+ 'publish_date',
10
+ ];
11
+ export const SIMPLE_METADATA_FIELDS = [
12
+ 'author',
13
+ 'description',
14
+ 'tags',
15
+ ];
16
+ export const FULL_METADATA_FIELDS = [
17
+ 'author',
18
+ 'description',
19
+ 'tags',
20
+ 'url',
21
+ 'fileKey',
22
+ ];
23
+ /** The fields to render: an explicit allow-list wins over the mode. */
24
+ export function resolveContentFields(mode, fields, simple, full) {
25
+ return new Set(fields ?? (mode === 'simple' ? simple : full));
26
+ }
@@ -9,4 +9,18 @@ export declare function createContentEditorImageRecord(apiBaseUrl: string, input
9
9
  mimeType: string;
10
10
  }): Promise<ContentEditorAsset | null>;
11
11
  export declare function resolveContentEditorImageSelection(apiBaseUrl: string, selected: ImageLike | File | string): Promise<ContentEditorAsset | null>;
12
+ /** Natural pixel size of an image, as used to choose a thumbnail placement. */
13
+ export interface ContentEditorImageDimensions {
14
+ width: number;
15
+ height: number;
16
+ }
17
+ /**
18
+ * The size of an attached image: from the asset's own `width`/`height` (or
19
+ * `metadata.width`/`metadata.height`) when recorded, otherwise by loading
20
+ * `src` in the browser. Resolves `null` when neither is available (no DOM,
21
+ * load error, or `timeoutMs` elapsed).
22
+ */
23
+ export declare function resolveContentEditorImageDimensions(asset: Record<string, unknown> | null | undefined, src: string, options?: {
24
+ timeoutMs?: number;
25
+ }): Promise<ContentEditorImageDimensions | null>;
12
26
  //# sourceMappingURL=content-editor-media.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"content-editor-media.d.ts","sourceRoot":"","sources":["../../src/svelte/content-editor-media.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClD,MAAM,CAUR;AAED,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAO1E;AAED,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmBzE;AAWD,wBAAsB,8BAA8B,CAClD,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE;IACL,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAgBpC;AAED,wBAAsB,kCAAkC,CACtD,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,SAAS,GAAG,IAAI,GAAG,MAAM,GAClC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA6BpC"}
1
+ {"version":3,"file":"content-editor-media.d.ts","sourceRoot":"","sources":["../../src/svelte/content-editor-media.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClD,MAAM,CAUR;AAED,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAO1E;AAED,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmBzE;AAWD,wBAAsB,8BAA8B,CAClD,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE;IACL,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAgBpC;AAED,wBAAsB,kCAAkC,CACtD,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,SAAS,GAAG,IAAI,GAAG,MAAM,GAClC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA6BpC;AAED,+EAA+E;AAC/E,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAOD;;;;;GAKG;AACH,wBAAsB,mCAAmC,CACvD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EACjD,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAmC9C"}
@@ -82,3 +82,44 @@ export async function resolveContentEditorImageSelection(apiBaseUrl, selected) {
82
82
  }
83
83
  return null;
84
84
  }
85
+ function positiveDimension(value) {
86
+ const number = Number(value);
87
+ return Number.isFinite(number) && number > 0 ? number : null;
88
+ }
89
+ /**
90
+ * The size of an attached image: from the asset's own `width`/`height` (or
91
+ * `metadata.width`/`metadata.height`) when recorded, otherwise by loading
92
+ * `src` in the browser. Resolves `null` when neither is available (no DOM,
93
+ * load error, or `timeoutMs` elapsed).
94
+ */
95
+ export async function resolveContentEditorImageDimensions(asset, src, options = {}) {
96
+ const metadata = asset?.metadata && typeof asset.metadata === 'object'
97
+ ? asset.metadata
98
+ : {};
99
+ const width = positiveDimension(asset?.width ?? metadata.width);
100
+ const height = positiveDimension(asset?.height ?? metadata.height);
101
+ if (width && height) {
102
+ return { width, height };
103
+ }
104
+ const ImageConstructor = globalThis.Image;
105
+ if (!src || typeof ImageConstructor !== 'function') {
106
+ return null;
107
+ }
108
+ return new Promise((resolve) => {
109
+ const image = new ImageConstructor();
110
+ const timer = setTimeout(() => resolve(null), options.timeoutMs ?? 8000);
111
+ image.onload = () => {
112
+ clearTimeout(timer);
113
+ const naturalWidth = positiveDimension(image.naturalWidth);
114
+ const naturalHeight = positiveDimension(image.naturalHeight);
115
+ resolve(naturalWidth && naturalHeight
116
+ ? { width: naturalWidth, height: naturalHeight }
117
+ : null);
118
+ };
119
+ image.onerror = () => {
120
+ clearTimeout(timer);
121
+ resolve(null);
122
+ };
123
+ image.src = src;
124
+ });
125
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ImageLike } from '@happyvertical/smrt-images/svelte';
2
+ import { type ContentMainPicture } from '../body-format.js';
2
3
  import { type ContentEditorAsset, type ContentEditorFormData, type ContentEditorInitialContent } from './content-editor-form.js';
3
4
  export interface CreateContentEditorStateOptions {
4
5
  content?: ContentEditorInitialContent | null;
@@ -9,6 +10,7 @@ export declare class ContentEditorState {
9
10
  private _fieldUndoStack;
10
11
  private _lastAppliedFields;
11
12
  private _showUndoBanner;
13
+ private _bodyImageSignature;
12
14
  constructor(options?: CreateContentEditorStateOptions);
13
15
  get form(): ContentEditorFormData;
14
16
  get snapshot(): {
@@ -30,6 +32,21 @@ export declare class ContentEditorState {
30
32
  addAsset(asset: ContentEditorAsset | ImageLike | Record<string, unknown>): void;
31
33
  removeAsset(assetId: string): void;
32
34
  setThumbnailAsset(assetId: string | null): void;
35
+ /**
36
+ * The main picture (thumbnail) as the story decides it: the picture the
37
+ * person chose, else the first picture in the story (see
38
+ * `resolveBodyMainPicture`).
39
+ */
40
+ get mainPicture(): ContentMainPicture;
41
+ /**
42
+ * Apply the main-picture rule after the story changed: the first picture
43
+ * in the story becomes the thumbnail unless one was chosen. Runs only
44
+ * when the story's pictures (or the choice) changed since the last call
45
+ * or `reset`, so editing text never replaces a thumbnail that was set
46
+ * some other way. A story without pictures keeps the current thumbnail.
47
+ * Returns true when the thumbnail changed.
48
+ */
49
+ syncMainPictureFromBody(): boolean;
33
50
  }
34
51
  export declare function createContentEditorState(options?: CreateContentEditorStateOptions): ContentEditorState;
35
52
  //# sourceMappingURL=content-editor-state.svelte.d.ts.map