@vit-foundation/ui 0.10.0 → 0.11.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/components/timeline/Timeline.svelte +47 -2
- package/dist/components/timeline/Timeline.svelte.d.ts +8 -0
- package/dist/components/timeline/TimelineMilestone.svelte +94 -38
- package/dist/components/timeline/TimelineMilestone.svelte.d.ts +16 -2
- package/dist/components/ui/Modal.svelte +1 -1
- package/dist/components/ui/RichText.svelte +12 -6
- package/dist/config/context.js +6 -1
- package/dist/config/edit-messages.d.ts +47 -0
- package/dist/config/edit-messages.js +24 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +1 -0
- package/dist/config/types.d.ts +8 -0
- package/dist/edit/Editable.svelte +5 -3
- package/dist/edit/chrome/AddSlot.svelte +89 -0
- package/dist/edit/chrome/AddSlot.svelte.d.ts +17 -0
- package/dist/edit/chrome/ConfirmDialog.svelte +65 -0
- package/dist/edit/chrome/ConfirmDialog.svelte.d.ts +18 -0
- package/dist/edit/chrome/EditFrame.svelte +171 -0
- package/dist/edit/chrome/EditFrame.svelte.d.ts +33 -0
- package/dist/edit/chrome/EditPanel.svelte +29 -0
- package/dist/edit/chrome/EditPanel.svelte.d.ts +15 -0
- package/dist/edit/chrome/EditPopover.svelte +57 -0
- package/dist/edit/chrome/EditPopover.svelte.d.ts +17 -0
- package/dist/edit/chrome/PropertyRow.svelte +241 -0
- package/dist/edit/chrome/PropertyRow.svelte.d.ts +17 -0
- package/dist/edit/helpers.d.ts +19 -1
- package/dist/edit/helpers.js +21 -0
- package/dist/edit/index.d.ts +8 -2
- package/dist/edit/index.js +6 -1
- package/dist/edit/types.d.ts +81 -1
- package/package.json +1 -1
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { getUiConfig } from '../../config/context.js';
|
|
3
3
|
import type { MilestoneData } from '../../content/types.js';
|
|
4
4
|
import type { MilestoneEditMap } from './TimelineMilestone.svelte';
|
|
5
|
+
import AddSlot from '../../edit/chrome/AddSlot.svelte';
|
|
6
|
+
import { getEditAdapter } from '../../edit/context.js';
|
|
7
|
+
import type { CollectionRef } from '../../edit/types.js';
|
|
5
8
|
import { yearOf } from '../../utils/dates.js';
|
|
6
9
|
import TimelineMilestone from './TimelineMilestone.svelte';
|
|
7
10
|
|
|
@@ -11,11 +14,30 @@
|
|
|
11
14
|
variant?: 'compact' | 'full';
|
|
12
15
|
/** Edit descriptors for one milestone's fields; see TimelineMilestone. */
|
|
13
16
|
editFor?: (milestone: MilestoneData) => MilestoneEditMap | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Names the collection these milestones are rows of. Set alongside an
|
|
19
|
+
* adapter with `applyOp`, it turns on the add slots and each frame's
|
|
20
|
+
* remove. NO reorder: order derives from `occurredOn` — editing the
|
|
21
|
+
* date IS the reorder.
|
|
22
|
+
*/
|
|
23
|
+
collection?: CollectionRef;
|
|
14
24
|
}
|
|
15
25
|
|
|
16
|
-
let { milestones, variant = 'compact', editFor }: Props = $props();
|
|
26
|
+
let { milestones, variant = 'compact', editFor, collection }: Props = $props();
|
|
17
27
|
|
|
18
28
|
const config = getUiConfig();
|
|
29
|
+
const adapter = getEditAdapter();
|
|
30
|
+
|
|
31
|
+
const structural = $derived(
|
|
32
|
+
collection !== undefined && (adapter?.isEditing ?? false) && adapter?.applyOp !== undefined
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** The map the host gave, plus removal — the list owns identity and order. */
|
|
36
|
+
function editMapFor(milestone: MilestoneData): MilestoneEditMap | undefined {
|
|
37
|
+
const map = editFor?.(milestone);
|
|
38
|
+
if (!structural || !collection) return map;
|
|
39
|
+
return { ...map, removeOp: { kind: 'remove', collection, id: milestone.id } };
|
|
40
|
+
}
|
|
19
41
|
|
|
20
42
|
interface TimelineEntry {
|
|
21
43
|
milestone: MilestoneData;
|
|
@@ -46,13 +68,29 @@
|
|
|
46
68
|
>
|
|
47
69
|
<ol>
|
|
48
70
|
{#each entries as entry (entry.milestone.id)}
|
|
71
|
+
{#if structural && collection}
|
|
72
|
+
<li class="add-slot">
|
|
73
|
+
<AddSlot
|
|
74
|
+
op={{
|
|
75
|
+
kind: 'create',
|
|
76
|
+
collection,
|
|
77
|
+
anchor: { id: entry.milestone.id, placement: 'before' }
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
</li>
|
|
81
|
+
{/if}
|
|
49
82
|
<li>
|
|
50
83
|
{#if entry.yearMarker}
|
|
51
84
|
<span class="year">{entry.yearMarker}</span>
|
|
52
85
|
{/if}
|
|
53
|
-
<TimelineMilestone milestone={entry.milestone} edit={
|
|
86
|
+
<TimelineMilestone milestone={entry.milestone} edit={editMapFor(entry.milestone)} />
|
|
54
87
|
</li>
|
|
55
88
|
{/each}
|
|
89
|
+
{#if structural && collection}
|
|
90
|
+
<li class="add-slot">
|
|
91
|
+
<AddSlot op={{ kind: 'create', collection }} />
|
|
92
|
+
</li>
|
|
93
|
+
{/if}
|
|
56
94
|
</ol>
|
|
57
95
|
</div>
|
|
58
96
|
|
|
@@ -92,6 +130,13 @@
|
|
|
92
130
|
flex-basis: 18rem;
|
|
93
131
|
}
|
|
94
132
|
|
|
133
|
+
/* The add affordances sit between the cards without taking a card's slot. */
|
|
134
|
+
li.add-slot,
|
|
135
|
+
.full li.add-slot {
|
|
136
|
+
flex: 0 0 auto;
|
|
137
|
+
align-self: center;
|
|
138
|
+
}
|
|
139
|
+
|
|
95
140
|
.year {
|
|
96
141
|
position: absolute;
|
|
97
142
|
top: -2.4rem;
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import type { MilestoneData } from '../../content/types.js';
|
|
2
2
|
import type { MilestoneEditMap } from './TimelineMilestone.svelte';
|
|
3
|
+
import type { CollectionRef } from '../../edit/types.js';
|
|
3
4
|
interface Props {
|
|
4
5
|
milestones: MilestoneData[];
|
|
5
6
|
/** 'compact' = homepage slice; 'full' = transparency history with year markers. */
|
|
6
7
|
variant?: 'compact' | 'full';
|
|
7
8
|
/** Edit descriptors for one milestone's fields; see TimelineMilestone. */
|
|
8
9
|
editFor?: (milestone: MilestoneData) => MilestoneEditMap | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Names the collection these milestones are rows of. Set alongside an
|
|
12
|
+
* adapter with `applyOp`, it turns on the add slots and each frame's
|
|
13
|
+
* remove. NO reorder: order derives from `occurredOn` — editing the
|
|
14
|
+
* date IS the reorder.
|
|
15
|
+
*/
|
|
16
|
+
collection?: CollectionRef;
|
|
9
17
|
}
|
|
10
18
|
declare const Timeline: import("svelte").Component<Props, {}, "">;
|
|
11
19
|
type Timeline = ReturnType<typeof Timeline>;
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
|
-
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
+
import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../../edit/types.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Which of the milestone's fields are editable at this render site:
|
|
6
|
+
* localized text inline (EditDescriptor), scalars through the frame's
|
|
7
|
+
* property panel (PropertyDescriptor), and removal from the collection.
|
|
8
|
+
*/
|
|
5
9
|
export interface MilestoneEditMap {
|
|
6
10
|
title?: EditDescriptor;
|
|
7
11
|
body?: EditDescriptor;
|
|
12
|
+
occurredOn?: PropertyDescriptor;
|
|
13
|
+
category?: PropertyDescriptor;
|
|
14
|
+
linkUrl?: PropertyDescriptor;
|
|
15
|
+
image?: PropertyDescriptor;
|
|
16
|
+
/** Accessible name for the frame, e.g. "Fita: Neix la fundació". */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Set by Timeline from its `collection` — removal of this milestone. */
|
|
19
|
+
removeOp?: Extract<EntityOp, { kind: 'remove' }>;
|
|
8
20
|
}
|
|
9
21
|
</script>
|
|
10
22
|
|
|
11
23
|
<script lang="ts">
|
|
12
24
|
import { getUiConfig } from '../../config/context.js';
|
|
13
25
|
import ActionLabel from '../../edit/ActionLabel.svelte';
|
|
14
|
-
import type { MilestoneData } from '../../content/types.js';
|
|
26
|
+
import type { MilestoneCategory, MilestoneData } from '../../content/types.js';
|
|
27
|
+
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
28
|
+
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
15
29
|
import Editable from '../../edit/Editable.svelte';
|
|
16
30
|
import { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from '../../utils/milestones.js';
|
|
17
31
|
import CardMedia from '../ui/CardMedia.svelte';
|
|
@@ -28,6 +42,41 @@
|
|
|
28
42
|
|
|
29
43
|
const config = getUiConfig();
|
|
30
44
|
|
|
45
|
+
const CATEGORIES = Object.keys(MILESTONE_CATEGORY_COLOR) as MilestoneCategory[];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The panel rows, zipped from the map's descriptors and this milestone's
|
|
49
|
+
* values. The category select's options come from the same labels the chip
|
|
50
|
+
* renders — filled here so the host never re-derives them.
|
|
51
|
+
*/
|
|
52
|
+
const panelRows = $derived(
|
|
53
|
+
[
|
|
54
|
+
edit?.occurredOn && { descriptor: edit.occurredOn, value: milestone.occurredOn },
|
|
55
|
+
edit?.category && {
|
|
56
|
+
descriptor: {
|
|
57
|
+
options: CATEGORIES.map((category) => ({
|
|
58
|
+
value: category,
|
|
59
|
+
label: milestoneCategoryLabel(category, config.messages)
|
|
60
|
+
})),
|
|
61
|
+
...edit.category
|
|
62
|
+
},
|
|
63
|
+
value: milestone.category
|
|
64
|
+
},
|
|
65
|
+
edit?.linkUrl && { descriptor: edit.linkUrl, value: milestone.linkUrl },
|
|
66
|
+
edit?.image && { descriptor: edit.image, value: milestone.imageUrls[0] ?? null }
|
|
67
|
+
].filter((row) => row !== undefined)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const frameSpec = $derived(
|
|
71
|
+
edit && (panelRows.length > 0 || edit.removeOp)
|
|
72
|
+
? {
|
|
73
|
+
label: edit.label ?? milestone.title,
|
|
74
|
+
hasPanel: panelRows.length > 0,
|
|
75
|
+
removeOp: edit.removeOp
|
|
76
|
+
}
|
|
77
|
+
: undefined
|
|
78
|
+
);
|
|
79
|
+
|
|
31
80
|
const color = $derived(MILESTONE_CATEGORY_COLOR[milestone.category]);
|
|
32
81
|
/**
|
|
33
82
|
* `link_url` is an unconstrained editable column, and the `press` category
|
|
@@ -48,42 +97,49 @@
|
|
|
48
97
|
|
|
49
98
|
<article style={`--milestone-color: ${color}`}>
|
|
50
99
|
<span class="dot" aria-hidden="true"></span>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
<!-- The frame sits INSIDE the article so the timeline's flex row never
|
|
101
|
+
gains an unexpected child; inactive it renders the content alone. -->
|
|
102
|
+
<EditFrame spec={frameSpec}>
|
|
103
|
+
{#snippet panel()}
|
|
104
|
+
<EditPanel rows={panelRows} />
|
|
105
|
+
{/snippet}
|
|
106
|
+
<Editable
|
|
107
|
+
edit={config.messageEdit?.(`category_${milestone.category}`)}
|
|
108
|
+
value={milestoneCategoryLabel(milestone.category, config.messages)}
|
|
109
|
+
>
|
|
110
|
+
{#snippet children(text, attrs)}<p class="category" {...attrs}>{text}</p>{/snippet}
|
|
111
|
+
</Editable>
|
|
112
|
+
<DateText value={milestone.occurredOn} />
|
|
113
|
+
<Editable edit={edit?.title} value={milestone.title}>
|
|
114
|
+
{#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
|
|
64
115
|
</Editable>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
>
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
{#if milestone.body}
|
|
117
|
+
<Editable edit={edit?.body} value={milestone.body}>
|
|
118
|
+
{#snippet children(text, attrs)}<p class="body" {...attrs}>{text}</p>{/snippet}
|
|
119
|
+
</Editable>
|
|
120
|
+
{/if}
|
|
121
|
+
{#if milestone.imageUrls.length > 0}
|
|
122
|
+
<CardMedia src={milestone.imageUrls[0]} alt="" width="1200" height="675" />
|
|
123
|
+
{/if}
|
|
124
|
+
{#if milestone.linkUrl}
|
|
125
|
+
<p class="more">
|
|
126
|
+
<ActionLabel
|
|
127
|
+
edit={config.messageEdit?.('common_readMore')}
|
|
128
|
+
value={config.messages.common_readMore()}
|
|
129
|
+
>
|
|
130
|
+
{#snippet control()}
|
|
131
|
+
{#if isExternal}
|
|
132
|
+
<a href={milestone.linkUrl} rel="external noopener"
|
|
133
|
+
>{config.messages.common_readMore()}</a
|
|
134
|
+
>
|
|
135
|
+
{:else if milestone.linkUrl}
|
|
136
|
+
<Link href={milestone.linkUrl}>{config.messages.common_readMore()}</Link>
|
|
137
|
+
{/if}
|
|
138
|
+
{/snippet}
|
|
139
|
+
</ActionLabel>
|
|
140
|
+
</p>
|
|
141
|
+
{/if}
|
|
142
|
+
</EditFrame>
|
|
87
143
|
</article>
|
|
88
144
|
|
|
89
145
|
<style>
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
-
/**
|
|
1
|
+
import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../../edit/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which of the milestone's fields are editable at this render site:
|
|
4
|
+
* localized text inline (EditDescriptor), scalars through the frame's
|
|
5
|
+
* property panel (PropertyDescriptor), and removal from the collection.
|
|
6
|
+
*/
|
|
3
7
|
export interface MilestoneEditMap {
|
|
4
8
|
title?: EditDescriptor;
|
|
5
9
|
body?: EditDescriptor;
|
|
10
|
+
occurredOn?: PropertyDescriptor;
|
|
11
|
+
category?: PropertyDescriptor;
|
|
12
|
+
linkUrl?: PropertyDescriptor;
|
|
13
|
+
image?: PropertyDescriptor;
|
|
14
|
+
/** Accessible name for the frame, e.g. "Fita: Neix la fundació". */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Set by Timeline from its `collection` — removal of this milestone. */
|
|
17
|
+
removeOp?: Extract<EntityOp, {
|
|
18
|
+
kind: 'remove';
|
|
19
|
+
}>;
|
|
6
20
|
}
|
|
7
21
|
import type { MilestoneData } from '../../content/types.js';
|
|
8
22
|
interface Props {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { getUiConfig } from '../../config/context.js';
|
|
2
3
|
import { renderBody } from '../../content/richtext.js';
|
|
3
4
|
import { getEditAdapter } from '../../edit/context.js';
|
|
4
5
|
import type { EditDescriptor } from '../../edit/types.js';
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
let { body, edit }: Props = $props();
|
|
24
25
|
|
|
25
26
|
const adapter = getEditAdapter();
|
|
27
|
+
const config = getUiConfig();
|
|
26
28
|
const canEdit = $derived(edit !== undefined && (adapter?.isEditing ?? false));
|
|
27
29
|
|
|
28
30
|
/** Last persisted body: what renders, and what Cancel·la returns to. */
|
|
@@ -79,16 +81,18 @@
|
|
|
79
81
|
class="control"
|
|
80
82
|
bind:value={draft}
|
|
81
83
|
rows={Math.max(6, draft.split('\n').length + 1)}
|
|
82
|
-
aria-label={edit?.label ??
|
|
84
|
+
aria-label={edit?.label ?? config.editMessages.edit_editBody()}
|
|
83
85
|
></textarea>
|
|
84
86
|
<div class="actions">
|
|
85
|
-
<Button pending={status === 'saving' ? 1 : 0} onclick={() => void save()}>
|
|
86
|
-
|
|
87
|
+
<Button pending={status === 'saving' ? 1 : 0} onclick={() => void save()}>
|
|
88
|
+
{config.editMessages.edit_save()}
|
|
89
|
+
</Button>
|
|
90
|
+
<GhostButton onclick={cancel}>{config.editMessages.edit_cancel()}</GhostButton>
|
|
87
91
|
{#if status === 'error'}
|
|
88
|
-
<p class="error" role="alert">
|
|
92
|
+
<p class="error" role="alert">{config.editMessages.edit_saveError()}. Torna-ho a provar.</p>
|
|
89
93
|
{/if}
|
|
90
94
|
</div>
|
|
91
|
-
<div class="richtext preview" aria-label=
|
|
95
|
+
<div class="richtext preview" aria-label={config.editMessages.edit_preview()}>
|
|
92
96
|
{#each preview as block, index (index)}
|
|
93
97
|
{#if block.type === 'h2'}
|
|
94
98
|
<h2>{block.text}</h2>
|
|
@@ -108,7 +112,9 @@
|
|
|
108
112
|
{/if}
|
|
109
113
|
{/each}
|
|
110
114
|
{#if canEdit}
|
|
111
|
-
<p class="edit-row"
|
|
115
|
+
<p class="edit-row">
|
|
116
|
+
<GhostButton onclick={open}>{config.editMessages.edit_edit()}</GhostButton>
|
|
117
|
+
</p>
|
|
112
118
|
{/if}
|
|
113
119
|
</div>
|
|
114
120
|
{/if}
|
package/dist/config/context.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { defaultEditMessages } from './edit-messages.js';
|
|
2
3
|
import { defaultMessages } from './messages.js';
|
|
3
4
|
import { BASE_LOCALE, LOCALES } from './types.js';
|
|
4
5
|
const KEY = Symbol.for('@vit-foundation/ui:config');
|
|
@@ -14,7 +15,8 @@ export const DEFAULT_UI_CONFIG = {
|
|
|
14
15
|
url: () => undefined,
|
|
15
16
|
canonicalPathname: (url) => url.pathname,
|
|
16
17
|
siteName: 'ViT',
|
|
17
|
-
messages: defaultMessages
|
|
18
|
+
messages: defaultMessages,
|
|
19
|
+
editMessages: defaultEditMessages
|
|
18
20
|
};
|
|
19
21
|
/**
|
|
20
22
|
* Installs the app's config for the subtree. Takes a *thunk* so a provider
|
|
@@ -38,6 +40,9 @@ export function setUiConfig(input) {
|
|
|
38
40
|
get messages() {
|
|
39
41
|
return input().messages ?? defaultMessages;
|
|
40
42
|
},
|
|
43
|
+
get editMessages() {
|
|
44
|
+
return input().editMessages ?? defaultEditMessages;
|
|
45
|
+
},
|
|
41
46
|
// Undefined means inert: components render their message strings as
|
|
42
47
|
// plain text, which is every app but the CMS.
|
|
43
48
|
messageEdit: (key) => input().messageEdit?.(key)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor chrome's own strings — status announcements, panel labels, the
|
|
3
|
+
* confirm-delete wording. A separate object from `UiMessages` on purpose:
|
|
4
|
+
* that interface mirrors the website's wording catalog (its keys are
|
|
5
|
+
* `ui_messages` rows a CMS edits), while these strings belong to the EDITOR
|
|
6
|
+
* a CMS host renders — they must never leak into the site's catalog, and
|
|
7
|
+
* they must never themselves become editable chrome.
|
|
8
|
+
*
|
|
9
|
+
* Same message-function style as `UiMessages`, and the same all-or-nothing
|
|
10
|
+
* override: a host replaces the whole object or keeps the Catalan defaults.
|
|
11
|
+
*/
|
|
12
|
+
export interface EditMessages {
|
|
13
|
+
/** Inline + panel status announcements. */
|
|
14
|
+
edit_saving(): string;
|
|
15
|
+
edit_saved(): string;
|
|
16
|
+
edit_saveError(): string;
|
|
17
|
+
/** RichText's source editor. */
|
|
18
|
+
edit_save(): string;
|
|
19
|
+
edit_cancel(): string;
|
|
20
|
+
edit_edit(): string;
|
|
21
|
+
edit_preview(): string;
|
|
22
|
+
edit_editBody(): string;
|
|
23
|
+
/** The frame toolbar and its panel. */
|
|
24
|
+
edit_properties(params: {
|
|
25
|
+
label: string;
|
|
26
|
+
}): string;
|
|
27
|
+
edit_remove(): string;
|
|
28
|
+
edit_removeConfirm(params: {
|
|
29
|
+
label: string;
|
|
30
|
+
}): string;
|
|
31
|
+
edit_close(): string;
|
|
32
|
+
/** Collections. */
|
|
33
|
+
edit_add(params: {
|
|
34
|
+
label: string;
|
|
35
|
+
}): string;
|
|
36
|
+
edit_addFailed(): string;
|
|
37
|
+
/** Property rows. */
|
|
38
|
+
edit_uploadImage(): string;
|
|
39
|
+
edit_clearValue(): string;
|
|
40
|
+
edit_emptyRequired(): string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Catalan defaults. The first three and the RichText strings reproduce the
|
|
44
|
+
* previously hardcoded copy byte-identically — swapping the source of a
|
|
45
|
+
* string must not repaint a single test or page.
|
|
46
|
+
*/
|
|
47
|
+
export declare const defaultEditMessages: EditMessages;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalan defaults. The first three and the RichText strings reproduce the
|
|
3
|
+
* previously hardcoded copy byte-identically — swapping the source of a
|
|
4
|
+
* string must not repaint a single test or page.
|
|
5
|
+
*/
|
|
6
|
+
export const defaultEditMessages = {
|
|
7
|
+
edit_saving: () => 'Desant…',
|
|
8
|
+
edit_saved: () => 'Desat',
|
|
9
|
+
edit_saveError: () => 'Error en desar',
|
|
10
|
+
edit_save: () => 'Desa',
|
|
11
|
+
edit_cancel: () => 'Cancel·la',
|
|
12
|
+
edit_edit: () => 'Edita',
|
|
13
|
+
edit_preview: () => 'Vista prèvia',
|
|
14
|
+
edit_editBody: () => 'Edita el contingut',
|
|
15
|
+
edit_properties: ({ label }) => `Propietats: ${label}`,
|
|
16
|
+
edit_remove: () => 'Elimina',
|
|
17
|
+
edit_removeConfirm: ({ label }) => `Segur que vols eliminar «${label}»? Es pot desfer des de l’historial.`,
|
|
18
|
+
edit_close: () => 'Tanca',
|
|
19
|
+
edit_add: ({ label }) => `Afegeix ${label}`,
|
|
20
|
+
edit_addFailed: () => 'No s’ha pogut afegir',
|
|
21
|
+
edit_uploadImage: () => 'Puja una imatge',
|
|
22
|
+
edit_clearValue: () => 'Buida el valor',
|
|
23
|
+
edit_emptyRequired: () => 'Aquest camp no pot quedar buit'
|
|
24
|
+
};
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { DEFAULT_UI_CONFIG, getUiConfig, setUiConfig } from './context.js';
|
|
2
|
+
export { defaultEditMessages } from './edit-messages.js';
|
|
3
|
+
export type { EditMessages } from './edit-messages.js';
|
|
2
4
|
export { defaultMessages } from './messages.js';
|
|
3
5
|
export { default as UiProvider } from './UiProvider.svelte';
|
|
4
6
|
export { BASE_LOCALE, LOCALES } from './types.js';
|
package/dist/config/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { DEFAULT_UI_CONFIG, getUiConfig, setUiConfig } from './context.js';
|
|
2
|
+
export { defaultEditMessages } from './edit-messages.js';
|
|
2
3
|
export { defaultMessages } from './messages.js';
|
|
3
4
|
export { default as UiProvider } from './UiProvider.svelte';
|
|
4
5
|
export { BASE_LOCALE, LOCALES } from './types.js';
|
package/dist/config/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EditDescriptor } from '../edit/types.js';
|
|
2
|
+
import type { EditMessages } from './edit-messages.js';
|
|
2
3
|
/**
|
|
3
4
|
* The site's locales and their canonical member. Fixed rather than generic:
|
|
4
5
|
* `ca` is load-bearing — it is the required key of every localized column
|
|
@@ -157,6 +158,12 @@ export interface UiConfig {
|
|
|
157
158
|
/** Appended to document titles and shown as the wordmark. */
|
|
158
159
|
siteName: string;
|
|
159
160
|
messages: UiMessages;
|
|
161
|
+
/**
|
|
162
|
+
* The editor chrome's own strings (status announcements, panel wording) —
|
|
163
|
+
* resolved, Catalan by default. NOT part of `messages`: see
|
|
164
|
+
* `./edit-messages.js` for why the two catalogs must not mix.
|
|
165
|
+
*/
|
|
166
|
+
editMessages: EditMessages;
|
|
160
167
|
/**
|
|
161
168
|
* Edit descriptor for one of THIS config's message strings, by catalog
|
|
162
169
|
* key — the interface-wording half of edit mode. Optional and undefined by
|
|
@@ -175,5 +182,6 @@ export interface UiConfigInput {
|
|
|
175
182
|
canonicalPathname?: UiConfig['canonicalPathname'];
|
|
176
183
|
siteName?: string;
|
|
177
184
|
messages?: UiMessages;
|
|
185
|
+
editMessages?: EditMessages;
|
|
178
186
|
messageEdit?: UiConfig['messageEdit'];
|
|
179
187
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { getUiConfig } from '../config/context.js';
|
|
3
4
|
import { getEditAdapter } from './context.js';
|
|
4
5
|
import type { EditDescriptor } from './types.js';
|
|
5
6
|
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
let { edit, value, children }: Props = $props();
|
|
56
57
|
|
|
57
58
|
const adapter = getEditAdapter();
|
|
59
|
+
const config = getUiConfig();
|
|
58
60
|
|
|
59
61
|
let status = $state<'idle' | 'dirty' | 'saving' | 'error'>('idle');
|
|
60
62
|
/** Last persisted value: what Escape restores and blur diffs against. */
|
|
@@ -121,16 +123,16 @@
|
|
|
121
123
|
return;
|
|
122
124
|
}
|
|
123
125
|
status = 'saving';
|
|
124
|
-
announcement =
|
|
126
|
+
announcement = config.editMessages.edit_saving();
|
|
125
127
|
try {
|
|
126
128
|
await adapter.save(edit, draft);
|
|
127
129
|
savedValue = draft;
|
|
128
130
|
status = 'idle';
|
|
129
|
-
announcement =
|
|
131
|
+
announcement = config.editMessages.edit_saved();
|
|
130
132
|
} catch {
|
|
131
133
|
// Draft stays in the DOM; the reader decides whether to retry.
|
|
132
134
|
status = 'error';
|
|
133
|
-
announcement =
|
|
135
|
+
announcement = config.editMessages.edit_saveError();
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Icon from '../../components/ui/Icon.svelte';
|
|
3
|
+
import { getUiConfig } from '../../config/context.js';
|
|
4
|
+
import { getEditAdapter } from '../context.js';
|
|
5
|
+
import type { EntityOp } from '../types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The "+" affordance of an editable collection. Renders ONLY while the
|
|
9
|
+
* adapter is editing AND implements `applyOp` — otherwise nothing, the
|
|
10
|
+
* byte-identical invariant. The op resolves server-side and the host's
|
|
11
|
+
* refresh brings the new row in; this control only shows pending/error.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
op: Extract<EntityOp, { kind: 'create' }>;
|
|
15
|
+
/** The entity noun for the label, e.g. "una fita". */
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { op, label = '' }: Props = $props();
|
|
20
|
+
|
|
21
|
+
const adapter = getEditAdapter();
|
|
22
|
+
const config = getUiConfig();
|
|
23
|
+
|
|
24
|
+
let pending = $state(false);
|
|
25
|
+
let announcement = $state('');
|
|
26
|
+
|
|
27
|
+
const active = $derived((adapter?.isEditing ?? false) && adapter?.applyOp !== undefined);
|
|
28
|
+
const text = $derived(config.editMessages.edit_add({ label }).trim());
|
|
29
|
+
|
|
30
|
+
async function add(): Promise<void> {
|
|
31
|
+
if (!adapter?.applyOp || pending) return;
|
|
32
|
+
pending = true;
|
|
33
|
+
announcement = '';
|
|
34
|
+
try {
|
|
35
|
+
await adapter.applyOp(op);
|
|
36
|
+
} catch {
|
|
37
|
+
announcement = config.editMessages.edit_addFailed();
|
|
38
|
+
} finally {
|
|
39
|
+
pending = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
{#if active}
|
|
45
|
+
<button type="button" class="add" disabled={pending} onclick={() => void add()}>
|
|
46
|
+
<Icon name="plus" size={16} />
|
|
47
|
+
{text}
|
|
48
|
+
</button>
|
|
49
|
+
<span class="status" role="status">{announcement}</span>
|
|
50
|
+
{/if}
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.add {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: var(--space-1);
|
|
57
|
+
padding: var(--space-1) var(--space-3);
|
|
58
|
+
border: 1px dashed var(--color-ink-muted);
|
|
59
|
+
border-radius: 999px;
|
|
60
|
+
background: transparent;
|
|
61
|
+
color: var(--color-ink);
|
|
62
|
+
font: inherit;
|
|
63
|
+
font-size: var(--text-sm);
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
transition: border-color var(--transition-fast);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.add:hover:not(:disabled) {
|
|
69
|
+
border-color: var(--color-brand);
|
|
70
|
+
color: var(--color-brand);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.add:disabled {
|
|
74
|
+
opacity: 0.5;
|
|
75
|
+
cursor: default;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.status {
|
|
79
|
+
position: absolute;
|
|
80
|
+
width: 1px;
|
|
81
|
+
height: 1px;
|
|
82
|
+
padding: 0;
|
|
83
|
+
margin: -1px;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
clip-path: inset(50%);
|
|
86
|
+
white-space: nowrap;
|
|
87
|
+
border: 0;
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EntityOp } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The "+" affordance of an editable collection. Renders ONLY while the
|
|
4
|
+
* adapter is editing AND implements `applyOp` — otherwise nothing, the
|
|
5
|
+
* byte-identical invariant. The op resolves server-side and the host's
|
|
6
|
+
* refresh brings the new row in; this control only shows pending/error.
|
|
7
|
+
*/
|
|
8
|
+
interface Props {
|
|
9
|
+
op: Extract<EntityOp, {
|
|
10
|
+
kind: 'create';
|
|
11
|
+
}>;
|
|
12
|
+
/** The entity noun for the label, e.g. "una fita". */
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const AddSlot: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type AddSlot = ReturnType<typeof AddSlot>;
|
|
17
|
+
export default AddSlot;
|