@vit-foundation/ui 0.13.0 → 0.14.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/account/NewsletterSignup.svelte +31 -10
- package/dist/components/jobs/JobList.svelte +6 -3
- package/dist/components/jobs/JobList.svelte.d.ts +2 -0
- package/dist/components/projects/ProjectCard.svelte +4 -1
- package/dist/components/projects/ProjectCard.svelte.d.ts +2 -0
- package/dist/components/timeline/TimelineMilestone.svelte +4 -1
- package/dist/components/timeline/TimelineMilestone.svelte.d.ts +2 -0
- package/dist/components/weeklies/CommentSection.svelte +23 -10
- package/dist/components/weeklies/WeeklieCard.svelte +6 -3
- package/dist/components/weeklies/WeeklieCard.svelte.d.ts +2 -0
- package/dist/config/edit-messages.d.ts +6 -0
- package/dist/config/edit-messages.js +4 -1
- package/dist/config/messages.js +2 -0
- package/dist/config/types.d.ts +7 -0
- package/dist/edit/chrome/LinkEdit.svelte +187 -0
- package/dist/edit/chrome/LinkEdit.svelte.d.ts +34 -0
- package/dist/edit/index.d.ts +1 -0
- package/dist/edit/index.js +1 -0
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { getUiConfig } from '../../config/context.js';
|
|
3
3
|
import ActionLabel from '../../edit/ActionLabel.svelte';
|
|
4
4
|
import Editable from '../../edit/Editable.svelte';
|
|
5
|
+
import LinkEdit from '../../edit/chrome/LinkEdit.svelte';
|
|
6
|
+
import { chromeProperty } from '../../edit/helpers.js';
|
|
5
7
|
import { withNewsletterIntent } from '../../forms/transport.js';
|
|
6
8
|
import type { NewsletterToggleFormInstance } from './AccountPanel.svelte';
|
|
7
9
|
import Button from '../ui/Button.svelte';
|
|
@@ -25,6 +27,19 @@
|
|
|
25
27
|
// verified — no app mail needed). Logged out, the band routes to the auth
|
|
26
28
|
// pages carrying the intent as ?newsletter=1 (never an email in a URL).
|
|
27
29
|
const f = $derived(newsletterToggleForm);
|
|
30
|
+
|
|
31
|
+
// The destinations are OPTIONAL catalog keys added after these components
|
|
32
|
+
// first shipped; a host without them keeps the built-in paths, and the
|
|
33
|
+
// intent query rides on whatever the key resolves to.
|
|
34
|
+
const signupHref = $derived(msg.comments_signupLinkHref?.() ?? '/signup');
|
|
35
|
+
const loginHref = $derived(msg.comments_loginLinkHref?.() ?? '/login');
|
|
36
|
+
// Gated like the form wording: descriptors exist only where the host
|
|
37
|
+
// exposes messageEdit; LinkEdit further requires saveProperty for the
|
|
38
|
+
// Adreça field.
|
|
39
|
+
const hrefProperty = (key: string) =>
|
|
40
|
+
config.messageEdit
|
|
41
|
+
? chromeProperty(key, { type: 'text', label: config.editMessages.edit_linkUrl() })
|
|
42
|
+
: undefined;
|
|
28
43
|
</script>
|
|
29
44
|
|
|
30
45
|
<section class="newsletter" aria-labelledby="newsletter-title">
|
|
@@ -44,23 +59,29 @@
|
|
|
44
59
|
>
|
|
45
60
|
{#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
|
|
46
61
|
</Editable>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
<LinkEdit
|
|
63
|
+
text={{
|
|
64
|
+
edit: config.messageEdit?.('comments_signupLink'),
|
|
65
|
+
value: msg.comments_signupLink()
|
|
66
|
+
}}
|
|
67
|
+
href={{ descriptor: hrefProperty('comments_signupLinkHref'), value: signupHref }}
|
|
50
68
|
>
|
|
51
|
-
{#snippet control()}<Link href={withNewsletterIntent(
|
|
69
|
+
{#snippet control()}<Link href={withNewsletterIntent(signupHref)}
|
|
52
70
|
>{msg.comments_signupLink()}</Link
|
|
53
71
|
>{/snippet}
|
|
54
|
-
</
|
|
72
|
+
</LinkEdit>
|
|
55
73
|
·
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
<LinkEdit
|
|
75
|
+
text={{
|
|
76
|
+
edit: config.messageEdit?.('comments_loginLink'),
|
|
77
|
+
value: msg.comments_loginLink()
|
|
78
|
+
}}
|
|
79
|
+
href={{ descriptor: hrefProperty('comments_loginLinkHref'), value: loginHref }}
|
|
59
80
|
>
|
|
60
|
-
{#snippet control()}<Link href={withNewsletterIntent(
|
|
81
|
+
{#snippet control()}<Link href={withNewsletterIntent(loginHref)}
|
|
61
82
|
>{msg.comments_loginLink()}</Link
|
|
62
83
|
>{/snippet}
|
|
63
|
-
</
|
|
84
|
+
</LinkEdit>
|
|
64
85
|
</p>
|
|
65
86
|
{:else if account.newsletterSubscribed}
|
|
66
87
|
<p class="prompt">
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
title?: EditDescriptor;
|
|
10
10
|
description?: EditDescriptor;
|
|
11
11
|
postedOn?: PropertyDescriptor;
|
|
12
|
+
/** Editorial state row; value derives from `draft` ('false' when draft). */
|
|
13
|
+
status?: PropertyDescriptor;
|
|
12
14
|
/** Accessible name for the frame — usually the opening's title. */
|
|
13
15
|
label?: string;
|
|
14
16
|
/** Set by the list from its `collection` — removal of this opening. */
|
|
@@ -75,9 +77,10 @@
|
|
|
75
77
|
<ul>
|
|
76
78
|
{#each jobs as job (job.slug)}
|
|
77
79
|
{@const edit = editMapFor(job)}
|
|
78
|
-
{@const rows = [
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
{@const rows = [
|
|
81
|
+
edit?.postedOn && { descriptor: edit.postedOn, value: job.postedOn },
|
|
82
|
+
edit?.status && { descriptor: edit.status, value: job.draft ? 'false' : 'true' }
|
|
83
|
+
].filter((row) => row !== undefined)}
|
|
81
84
|
<li>
|
|
82
85
|
<!-- Inside the li — see CollaboratorList. -->
|
|
83
86
|
<EditFrame
|
|
@@ -7,6 +7,8 @@ export interface JobEditMap {
|
|
|
7
7
|
title?: EditDescriptor;
|
|
8
8
|
description?: EditDescriptor;
|
|
9
9
|
postedOn?: PropertyDescriptor;
|
|
10
|
+
/** Editorial state row; value derives from `draft` ('false' when draft). */
|
|
11
|
+
status?: PropertyDescriptor;
|
|
10
12
|
/** Accessible name for the frame — usually the opening's title. */
|
|
11
13
|
label?: string;
|
|
12
14
|
/** Set by the list from its `collection` — removal of this opening. */
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
publishedOn?: PropertyDescriptor;
|
|
13
13
|
externalUrl?: PropertyDescriptor;
|
|
14
14
|
image?: PropertyDescriptor;
|
|
15
|
+
/** Editorial state row; value derives from `draft` ('false' when draft). */
|
|
16
|
+
status?: PropertyDescriptor;
|
|
15
17
|
/** Accessible name for the frame, e.g. the project's title. */
|
|
16
18
|
label?: string;
|
|
17
19
|
}
|
|
@@ -67,7 +69,8 @@
|
|
|
67
69
|
},
|
|
68
70
|
edit?.publishedOn && { descriptor: edit.publishedOn, value: project.publishedOn },
|
|
69
71
|
edit?.externalUrl && { descriptor: edit.externalUrl, value: project.externalUrl },
|
|
70
|
-
edit?.image && { descriptor: edit.image, value: project.imageUrl }
|
|
72
|
+
edit?.image && { descriptor: edit.image, value: project.imageUrl },
|
|
73
|
+
edit?.status && { descriptor: edit.status, value: project.draft ? 'false' : 'true' }
|
|
71
74
|
].filter((row) => row !== undefined)
|
|
72
75
|
);
|
|
73
76
|
const frameSpec = $derived(
|
|
@@ -10,6 +10,8 @@ export interface ProjectEditMap {
|
|
|
10
10
|
publishedOn?: PropertyDescriptor;
|
|
11
11
|
externalUrl?: PropertyDescriptor;
|
|
12
12
|
image?: PropertyDescriptor;
|
|
13
|
+
/** Editorial state row; value derives from `draft` ('false' when draft). */
|
|
14
|
+
status?: PropertyDescriptor;
|
|
13
15
|
/** Accessible name for the frame, e.g. the project's title. */
|
|
14
16
|
label?: string;
|
|
15
17
|
}
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
category?: PropertyDescriptor;
|
|
14
14
|
linkUrl?: PropertyDescriptor;
|
|
15
15
|
image?: PropertyDescriptor;
|
|
16
|
+
/** Editorial state row; value derives from `draft` ('false' when draft). */
|
|
17
|
+
status?: PropertyDescriptor;
|
|
16
18
|
/** Accessible name for the frame, e.g. "Fita: Neix la fundació". */
|
|
17
19
|
label?: string;
|
|
18
20
|
/** Set by Timeline from its `collection` — removal of this milestone. */
|
|
@@ -64,7 +66,8 @@
|
|
|
64
66
|
value: milestone.category
|
|
65
67
|
},
|
|
66
68
|
edit?.linkUrl && { descriptor: edit.linkUrl, value: milestone.linkUrl },
|
|
67
|
-
edit?.image && { descriptor: edit.image, value: milestone.imageUrls[0] ?? null }
|
|
69
|
+
edit?.image && { descriptor: edit.image, value: milestone.imageUrls[0] ?? null },
|
|
70
|
+
edit?.status && { descriptor: edit.status, value: milestone.draft ? 'false' : 'true' }
|
|
68
71
|
].filter((row) => row !== undefined)
|
|
69
72
|
);
|
|
70
73
|
|
|
@@ -11,6 +11,8 @@ export interface MilestoneEditMap {
|
|
|
11
11
|
category?: PropertyDescriptor;
|
|
12
12
|
linkUrl?: PropertyDescriptor;
|
|
13
13
|
image?: PropertyDescriptor;
|
|
14
|
+
/** Editorial state row; value derives from `draft` ('false' when draft). */
|
|
15
|
+
status?: PropertyDescriptor;
|
|
14
16
|
/** Accessible name for the frame, e.g. "Fita: Neix la fundació". */
|
|
15
17
|
label?: string;
|
|
16
18
|
/** Set by Timeline from its `collection` — removal of this milestone. */
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
<script lang="ts">
|
|
12
12
|
import { getUiConfig } from '../../config/context.js';
|
|
13
13
|
import ActionLabel from '../../edit/ActionLabel.svelte';
|
|
14
|
+
import LinkEdit from '../../edit/chrome/LinkEdit.svelte';
|
|
15
|
+
import { chromeProperty } from '../../edit/helpers.js';
|
|
14
16
|
import Editable from '../../edit/Editable.svelte';
|
|
15
17
|
import { COMMENT_BODY } from '../../forms/constraints.js';
|
|
16
18
|
import type { CommentData, CommentThreadData } from '../../content/types.js';
|
|
@@ -52,6 +54,14 @@
|
|
|
52
54
|
const main = $derived(commentForm);
|
|
53
55
|
|
|
54
56
|
const errorMessages = $derived({ unauthenticated: msg.comments_loginPrompt() });
|
|
57
|
+
|
|
58
|
+
// Optional catalog keys; hosts without them keep the built-in paths.
|
|
59
|
+
const signupHref = $derived(msg.comments_signupLinkHref?.() ?? '/signup');
|
|
60
|
+
const loginHref = $derived(msg.comments_loginLinkHref?.() ?? '/login');
|
|
61
|
+
const hrefProperty = (key: string) =>
|
|
62
|
+
config.messageEdit
|
|
63
|
+
? chromeProperty(key, { type: 'text', label: config.editMessages.edit_linkUrl() })
|
|
64
|
+
: undefined;
|
|
55
65
|
</script>
|
|
56
66
|
|
|
57
67
|
{#snippet commentMeta(comment: CommentData)}
|
|
@@ -180,19 +190,22 @@
|
|
|
180
190
|
>
|
|
181
191
|
{#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
|
|
182
192
|
</Editable>
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
<LinkEdit
|
|
194
|
+
text={{ edit: config.messageEdit?.('comments_loginLink'), value: msg.comments_loginLink() }}
|
|
195
|
+
href={{ descriptor: hrefProperty('comments_loginLinkHref'), value: loginHref }}
|
|
186
196
|
>
|
|
187
|
-
{#snippet control()}<Link href=
|
|
188
|
-
</
|
|
197
|
+
{#snippet control()}<Link href={loginHref}>{msg.comments_loginLink()}</Link>{/snippet}
|
|
198
|
+
</LinkEdit>
|
|
189
199
|
·
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
200
|
+
<LinkEdit
|
|
201
|
+
text={{
|
|
202
|
+
edit: config.messageEdit?.('comments_signupLink'),
|
|
203
|
+
value: msg.comments_signupLink()
|
|
204
|
+
}}
|
|
205
|
+
href={{ descriptor: hrefProperty('comments_signupLinkHref'), value: signupHref }}
|
|
193
206
|
>
|
|
194
|
-
{#snippet control()}<Link href=
|
|
195
|
-
</
|
|
207
|
+
{#snippet control()}<Link href={signupHref}>{msg.comments_signupLink()}</Link>{/snippet}
|
|
208
|
+
</LinkEdit>
|
|
196
209
|
</p>
|
|
197
210
|
{/if}
|
|
198
211
|
</section>
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
title?: EditDescriptor;
|
|
11
11
|
excerpt?: EditDescriptor;
|
|
12
12
|
image?: PropertyDescriptor;
|
|
13
|
+
/** Editorial state row; value derives from `draft` ('false' when draft). */
|
|
14
|
+
status?: PropertyDescriptor;
|
|
13
15
|
/** Accessible name for the frame, e.g. "Weekly #12". */
|
|
14
16
|
label?: string;
|
|
15
17
|
}
|
|
@@ -45,9 +47,10 @@
|
|
|
45
47
|
const titleEditing = $derived(edit?.title !== undefined && (adapter?.isEditing ?? false));
|
|
46
48
|
|
|
47
49
|
const panelRows = $derived(
|
|
48
|
-
[
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
[
|
|
51
|
+
edit?.image && { descriptor: edit.image, value: weekly.imageUrl },
|
|
52
|
+
edit?.status && { descriptor: edit.status, value: weekly.draft ? 'false' : 'true' }
|
|
53
|
+
].filter((row) => row !== undefined)
|
|
51
54
|
);
|
|
52
55
|
const frameSpec = $derived(
|
|
53
56
|
edit && panelRows.length > 0 ? { label: edit.label ?? weekly.title, hasPanel: true } : undefined
|
|
@@ -8,6 +8,8 @@ export interface WeeklyEditMap {
|
|
|
8
8
|
title?: EditDescriptor;
|
|
9
9
|
excerpt?: EditDescriptor;
|
|
10
10
|
image?: PropertyDescriptor;
|
|
11
|
+
/** Editorial state row; value derives from `draft` ('false' when draft). */
|
|
12
|
+
status?: PropertyDescriptor;
|
|
11
13
|
/** Accessible name for the frame, e.g. "Weekly #12". */
|
|
12
14
|
label?: string;
|
|
13
15
|
}
|
|
@@ -40,6 +40,12 @@ export interface EditMessages {
|
|
|
40
40
|
edit_draftBadge(): string;
|
|
41
41
|
edit_clearValue(): string;
|
|
42
42
|
edit_emptyRequired(): string;
|
|
43
|
+
/** The link modal: one dialog edits a link's text and destination. */
|
|
44
|
+
edit_editLink(params: {
|
|
45
|
+
label: string;
|
|
46
|
+
}): string;
|
|
47
|
+
edit_linkText(): string;
|
|
48
|
+
edit_linkUrl(): string;
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
51
|
* Catalan defaults. The first three and the RichText strings reproduce the
|
|
@@ -21,5 +21,8 @@ export const defaultEditMessages = {
|
|
|
21
21
|
edit_uploadImage: () => 'Puja una imatge',
|
|
22
22
|
edit_draftBadge: () => 'Esborrany',
|
|
23
23
|
edit_clearValue: () => 'Buida el valor',
|
|
24
|
-
edit_emptyRequired: () => 'Aquest camp no pot quedar buit'
|
|
24
|
+
edit_emptyRequired: () => 'Aquest camp no pot quedar buit',
|
|
25
|
+
edit_editLink: ({ label }) => `Edita l’enllaç «${label}»`,
|
|
26
|
+
edit_linkText: () => 'Text',
|
|
27
|
+
edit_linkUrl: () => 'Adreça'
|
|
25
28
|
};
|
package/dist/config/messages.js
CHANGED
|
@@ -82,6 +82,8 @@ export const defaultMessages = {
|
|
|
82
82
|
comments_loginPrompt: () => 'Per comentar cal iniciar la sessió.',
|
|
83
83
|
comments_loginLink: () => 'Inicia la sessió',
|
|
84
84
|
comments_signupLink: () => 'Crea un compte',
|
|
85
|
+
comments_loginLinkHref: () => '/login',
|
|
86
|
+
comments_signupLinkHref: () => '/signup',
|
|
85
87
|
reaction_like: () => "M'agrada",
|
|
86
88
|
reaction_love: () => "M'encanta",
|
|
87
89
|
reaction_clap: () => 'Aplaudiment',
|
package/dist/config/types.d.ts
CHANGED
|
@@ -114,6 +114,13 @@ export interface UiMessages {
|
|
|
114
114
|
comments_loginPrompt(): string;
|
|
115
115
|
comments_loginLink(): string;
|
|
116
116
|
comments_signupLink(): string;
|
|
117
|
+
/**
|
|
118
|
+
* OPTIONAL — the login/signup destinations, added after the catalogs these
|
|
119
|
+
* components first shipped with. A host without the keys keeps the
|
|
120
|
+
* built-in /login and /signup paths, byte-identically.
|
|
121
|
+
*/
|
|
122
|
+
comments_loginLinkHref?(): string;
|
|
123
|
+
comments_signupLinkHref?(): string;
|
|
117
124
|
reaction_like(): string;
|
|
118
125
|
reaction_love(): string;
|
|
119
126
|
reaction_clap(): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Button from '../../components/ui/Button.svelte';
|
|
4
|
+
import Icon from '../../components/ui/Icon.svelte';
|
|
5
|
+
import Modal from '../../components/ui/Modal.svelte';
|
|
6
|
+
import { getUiConfig } from '../../config/context.js';
|
|
7
|
+
import { getEditAdapter } from '../context.js';
|
|
8
|
+
import type { EditDescriptor, PropertyDescriptor } from '../types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A LINK edited whole: one modal for its text and its destination, instead
|
|
12
|
+
* of an inline label swap plus a separate property panel — a link's two
|
|
13
|
+
* halves belong to one gesture.
|
|
14
|
+
*
|
|
15
|
+
* While the adapter is editing and a text descriptor is supplied, the
|
|
16
|
+
* control is REPLACED by a button wearing the label (the ActionLabel rule:
|
|
17
|
+
* a caret or a click inside a live link would navigate mid-edit) that
|
|
18
|
+
* opens the modal. Desa commits ONLY the halves that changed — the text
|
|
19
|
+
* through `adapter.save`, the destination through `adapter.saveProperty` —
|
|
20
|
+
* so a failure on one keeps the other's success and the modal stays open
|
|
21
|
+
* with the reason. Without an adapter or a descriptor the control renders
|
|
22
|
+
* untouched, byte-identical to a read-only build.
|
|
23
|
+
*/
|
|
24
|
+
interface Props {
|
|
25
|
+
/** Inline-label half; undefined leaves the control alone. */
|
|
26
|
+
text: { edit: EditDescriptor | undefined; value: string };
|
|
27
|
+
/** Destination half; undefined hides the Adreça field. */
|
|
28
|
+
href: { descriptor: PropertyDescriptor | undefined; value: string };
|
|
29
|
+
/** Accessible name for the modal, e.g. the link's current text. */
|
|
30
|
+
label?: string;
|
|
31
|
+
control: Snippet;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let { text, href, label = undefined, control }: Props = $props();
|
|
35
|
+
|
|
36
|
+
const adapter = getEditAdapter();
|
|
37
|
+
const config = getUiConfig();
|
|
38
|
+
const messages = $derived(config.editMessages);
|
|
39
|
+
|
|
40
|
+
const editing = $derived(text.edit !== undefined && (adapter?.isEditing ?? false));
|
|
41
|
+
// The Adreça field needs both a place to save to and a verb to save with.
|
|
42
|
+
const editsHref = $derived(href.descriptor !== undefined && adapter?.saveProperty !== undefined);
|
|
43
|
+
|
|
44
|
+
let open = $state(false);
|
|
45
|
+
let draftText = $state('');
|
|
46
|
+
let draftHref = $state('');
|
|
47
|
+
let saving = $state(false);
|
|
48
|
+
let error = $state<string | null>(null);
|
|
49
|
+
|
|
50
|
+
function show(): void {
|
|
51
|
+
draftText = text.value;
|
|
52
|
+
draftHref = href.value;
|
|
53
|
+
error = null;
|
|
54
|
+
open = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Escape/Cancel·la/backdrop: discard the drafts, keep the stored values. */
|
|
58
|
+
function cancel(): void {
|
|
59
|
+
if (saving) return;
|
|
60
|
+
open = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function save(): Promise<void> {
|
|
64
|
+
if (!adapter || text.edit === undefined) return;
|
|
65
|
+
const nextText = draftText.trim();
|
|
66
|
+
const nextHref = draftHref.trim();
|
|
67
|
+
saving = true;
|
|
68
|
+
error = null;
|
|
69
|
+
try {
|
|
70
|
+
// Sequential on purpose: if the text lands and the address fails, the
|
|
71
|
+
// reseeded draft no longer differs, so a retry commits only what is
|
|
72
|
+
// still pending.
|
|
73
|
+
if (nextText !== text.value) {
|
|
74
|
+
await adapter.save(text.edit, nextText);
|
|
75
|
+
draftText = nextText;
|
|
76
|
+
}
|
|
77
|
+
if (editsHref && href.descriptor && nextHref !== href.value) {
|
|
78
|
+
await adapter.saveProperty!(href.descriptor, nextHref);
|
|
79
|
+
draftHref = nextHref;
|
|
80
|
+
}
|
|
81
|
+
open = false;
|
|
82
|
+
} catch (thrown) {
|
|
83
|
+
error =
|
|
84
|
+
thrown instanceof Error && thrown.message ? thrown.message : messages.edit_saveError();
|
|
85
|
+
} finally {
|
|
86
|
+
saving = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
{#if editing}
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
class="link-swap"
|
|
95
|
+
data-vit-editing="idle"
|
|
96
|
+
aria-label={messages.edit_editLink({ label: label ?? text.value })}
|
|
97
|
+
onclick={show}
|
|
98
|
+
>
|
|
99
|
+
{text.value}
|
|
100
|
+
<Icon name="pencil" size={13} />
|
|
101
|
+
</button>
|
|
102
|
+
<Modal
|
|
103
|
+
{open}
|
|
104
|
+
title={messages.edit_editLink({ label: label ?? text.value })}
|
|
105
|
+
onclose={cancel}
|
|
106
|
+
closeLabel={messages.edit_close()}
|
|
107
|
+
>
|
|
108
|
+
<div class="fields">
|
|
109
|
+
<label>
|
|
110
|
+
<span>{messages.edit_linkText()}</span>
|
|
111
|
+
<input type="text" bind:value={draftText} disabled={saving} />
|
|
112
|
+
</label>
|
|
113
|
+
{#if editsHref}
|
|
114
|
+
<label>
|
|
115
|
+
<span>{messages.edit_linkUrl()}</span>
|
|
116
|
+
<!-- type=text, not url: site destinations are relative paths. -->
|
|
117
|
+
<input type="text" bind:value={draftHref} disabled={saving} />
|
|
118
|
+
</label>
|
|
119
|
+
{/if}
|
|
120
|
+
</div>
|
|
121
|
+
{#if error}
|
|
122
|
+
<p class="error" role="alert">{error}</p>
|
|
123
|
+
{/if}
|
|
124
|
+
<div class="actions">
|
|
125
|
+
<Button pending={saving ? 1 : 0} onclick={() => void save()}>{messages.edit_save()}</Button>
|
|
126
|
+
<Button variant="ghost" pending={null} onclick={cancel}>{messages.edit_cancel()}</Button>
|
|
127
|
+
</div>
|
|
128
|
+
</Modal>
|
|
129
|
+
{:else}
|
|
130
|
+
{@render control()}
|
|
131
|
+
{/if}
|
|
132
|
+
|
|
133
|
+
<style>
|
|
134
|
+
/* The ActionLabel footprint plus a pencil hint: the same dashed affordance
|
|
135
|
+
(base.css paints [data-vit-editing]), but a click OPENS rather than
|
|
136
|
+
focuses — this swap is a button, never a caret. */
|
|
137
|
+
.link-swap {
|
|
138
|
+
display: inline-flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: var(--space-1);
|
|
141
|
+
padding: 0.2em 0.4em;
|
|
142
|
+
border: none;
|
|
143
|
+
background: transparent;
|
|
144
|
+
font: inherit;
|
|
145
|
+
color: inherit;
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.fields {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: var(--space-3);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
label {
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
gap: var(--space-1);
|
|
159
|
+
font-size: var(--text-sm);
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* PropertyRow's control look — the modal is the panel, relocated. */
|
|
164
|
+
input {
|
|
165
|
+
font: inherit;
|
|
166
|
+
font-size: var(--text-sm);
|
|
167
|
+
font-weight: 400;
|
|
168
|
+
color: var(--color-ink);
|
|
169
|
+
padding: var(--space-1) var(--space-2);
|
|
170
|
+
border: 1px solid var(--color-hairline);
|
|
171
|
+
border-radius: var(--radius);
|
|
172
|
+
background: var(--color-surface);
|
|
173
|
+
width: 100%;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.error {
|
|
177
|
+
margin: var(--space-3) 0 0;
|
|
178
|
+
color: var(--series-8);
|
|
179
|
+
font-size: var(--text-sm);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.actions {
|
|
183
|
+
display: flex;
|
|
184
|
+
gap: var(--space-2);
|
|
185
|
+
margin-top: var(--space-4);
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { EditDescriptor, PropertyDescriptor } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A LINK edited whole: one modal for its text and its destination, instead
|
|
5
|
+
* of an inline label swap plus a separate property panel — a link's two
|
|
6
|
+
* halves belong to one gesture.
|
|
7
|
+
*
|
|
8
|
+
* While the adapter is editing and a text descriptor is supplied, the
|
|
9
|
+
* control is REPLACED by a button wearing the label (the ActionLabel rule:
|
|
10
|
+
* a caret or a click inside a live link would navigate mid-edit) that
|
|
11
|
+
* opens the modal. Desa commits ONLY the halves that changed — the text
|
|
12
|
+
* through `adapter.save`, the destination through `adapter.saveProperty` —
|
|
13
|
+
* so a failure on one keeps the other's success and the modal stays open
|
|
14
|
+
* with the reason. Without an adapter or a descriptor the control renders
|
|
15
|
+
* untouched, byte-identical to a read-only build.
|
|
16
|
+
*/
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Inline-label half; undefined leaves the control alone. */
|
|
19
|
+
text: {
|
|
20
|
+
edit: EditDescriptor | undefined;
|
|
21
|
+
value: string;
|
|
22
|
+
};
|
|
23
|
+
/** Destination half; undefined hides the Adreça field. */
|
|
24
|
+
href: {
|
|
25
|
+
descriptor: PropertyDescriptor | undefined;
|
|
26
|
+
value: string;
|
|
27
|
+
};
|
|
28
|
+
/** Accessible name for the modal, e.g. the link's current text. */
|
|
29
|
+
label?: string;
|
|
30
|
+
control: Snippet;
|
|
31
|
+
}
|
|
32
|
+
declare const LinkEdit: import("svelte").Component<Props, {}, "">;
|
|
33
|
+
type LinkEdit = ReturnType<typeof LinkEdit>;
|
|
34
|
+
export default LinkEdit;
|
package/dist/edit/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { default as EditFrame } from './chrome/EditFrame.svelte';
|
|
|
7
7
|
export type { EditFrameSpec } from './chrome/EditFrame.svelte';
|
|
8
8
|
export { default as EditPanel } from './chrome/EditPanel.svelte';
|
|
9
9
|
export { default as EditPopover } from './chrome/EditPopover.svelte';
|
|
10
|
+
export { default as LinkEdit } from './chrome/LinkEdit.svelte';
|
|
10
11
|
export { chromeEdit, chromeProperty, collectionOf, entityEdit, entityProperty, pageCopyEdit } from './helpers.js';
|
|
11
12
|
export { localize } from './types.js';
|
|
12
13
|
export type { CollectionRef, ContentRef, EditableEntity, EditAdapter, EditDescriptor, EntityOp, LocalizedText, PropertyDescriptor, PropertyOption, PropertyType } from './types.js';
|
package/dist/edit/index.js
CHANGED
|
@@ -6,5 +6,6 @@ export { default as ConfirmDialog } from './chrome/ConfirmDialog.svelte';
|
|
|
6
6
|
export { default as EditFrame } from './chrome/EditFrame.svelte';
|
|
7
7
|
export { default as EditPanel } from './chrome/EditPanel.svelte';
|
|
8
8
|
export { default as EditPopover } from './chrome/EditPopover.svelte';
|
|
9
|
+
export { default as LinkEdit } from './chrome/LinkEdit.svelte';
|
|
9
10
|
export { chromeEdit, chromeProperty, collectionOf, entityEdit, entityProperty, pageCopyEdit } from './helpers.js';
|
|
10
11
|
export { localize } from './types.js';
|