@portaki/module-sections 1.2.7 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/portaki.catalog.json +2 -2
- package/portaki.module.json +3 -3
- package/src/components/SectionsGuestView.tsx +3 -7
- package/src/host/SectionCreateModal.tsx +159 -0
- package/src/host/SectionsHostWorkspace.tsx +497 -0
- package/src/host/section-tiptap-presets.ts +156 -0
- package/src/host/sections-editor-design.ts +79 -0
- package/src/host/sections-host.types.ts +53 -0
- package/src/host/sections-toast-copy.ts +9 -0
- package/src/module.test.tsx +19 -4
- package/src/{sections-module-definition.ts → sections-module-definition.tsx} +131 -49
- package/src/test/mock-host-module-context.ts +35 -0
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portaki/module-sections",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Portaki module — editorial sections (TipTap)",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.tsx",
|
|
8
8
|
"dependencies": {
|
|
9
|
+
"@dnd-kit/core": "^6.3.1",
|
|
10
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
11
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
9
12
|
"@portaki/sdk": "^3.0.0",
|
|
13
|
+
"lucide-react": "^1.14.0",
|
|
10
14
|
"@tiptap/core": "^3.0.0",
|
|
11
15
|
"@tiptap/html": "^3.0.0",
|
|
12
16
|
"@tiptap/starter-kit": "^3.0.0",
|
package/portaki.catalog.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"fr": "Blocs de contenu riches (TipTap) pour le carnet d’accueil invité.",
|
|
10
10
|
"en": "Rich content blocks (TipTap) for the guest welcome book."
|
|
11
11
|
},
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.3.1",
|
|
13
13
|
"releaseNotesUrl": "https://github.com/PortakiApp/portaki-modules/releases",
|
|
14
14
|
"changelog": [
|
|
15
15
|
{
|
|
@@ -122,6 +122,6 @@
|
|
|
122
122
|
},
|
|
123
123
|
"artifacts": {
|
|
124
124
|
"wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections",
|
|
125
|
-
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.
|
|
125
|
+
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.3.1"
|
|
126
126
|
}
|
|
127
127
|
}
|
package/portaki.module.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"fr": "Blocs de contenu riches (TipTap) pour le carnet d’accueil invité.",
|
|
10
10
|
"en": "Rich content blocks (TipTap) for the guest welcome book."
|
|
11
11
|
},
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.3.1",
|
|
13
13
|
"releaseNotesUrl": "https://github.com/PortakiApp/portaki-modules/releases",
|
|
14
14
|
"changelog": [
|
|
15
15
|
{
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"portakiVersionMin": "1.0.0",
|
|
45
45
|
"requiresHostSdk": "3.0.0",
|
|
46
46
|
"database": {
|
|
47
|
-
"schemaVersion": "1.0.
|
|
47
|
+
"schemaVersion": "1.0.2"
|
|
48
48
|
},
|
|
49
49
|
"license": "AGPL-3.0",
|
|
50
50
|
"repository": "https://github.com/PortakiApp/portaki-modules",
|
|
@@ -122,6 +122,6 @@
|
|
|
122
122
|
},
|
|
123
123
|
"artifacts": {
|
|
124
124
|
"wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections",
|
|
125
|
-
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.
|
|
125
|
+
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.3.1"
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -16,10 +16,6 @@ type SectionRow = {
|
|
|
16
16
|
contentEn: unknown
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type ListResponse = {
|
|
20
|
-
sections: SectionRow[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
const htmlExtensions = [StarterKit]
|
|
24
20
|
|
|
25
21
|
function contentToHtml(raw: unknown): string {
|
|
@@ -33,7 +29,7 @@ function contentToHtml(raw: unknown): string {
|
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
export function SectionsGuestView({ lang }: { lang: LangCode }) {
|
|
36
|
-
const { data, loading, error } = usePortakiQuery<
|
|
32
|
+
const { data, loading, error } = usePortakiQuery<SectionRow[]>('sections.list', {})
|
|
37
33
|
|
|
38
34
|
if (loading) {
|
|
39
35
|
return (
|
|
@@ -43,13 +39,13 @@ export function SectionsGuestView({ lang }: { lang: LangCode }) {
|
|
|
43
39
|
)
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
if (error != null || !data?.
|
|
42
|
+
if (error != null || !data?.length) {
|
|
47
43
|
return null
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
return (
|
|
51
47
|
<div className="space-y-10">
|
|
52
|
-
{data.
|
|
48
|
+
{data.map((section) => {
|
|
53
49
|
const title = lang === 'en' ? section.titleEn || section.titleFr : section.titleFr
|
|
54
50
|
const html = contentToHtml(lang === 'en' ? section.contentEn : section.contentFr)
|
|
55
51
|
return (
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { HostModuleContext } from '@portaki/sdk'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
SectionContentPresetId,
|
|
8
|
+
SectionCreateModalDesign,
|
|
9
|
+
SectionTemplateDesign,
|
|
10
|
+
} from './sections-host.types'
|
|
11
|
+
|
|
12
|
+
export type SectionCreatePayload = {
|
|
13
|
+
titleFr: string
|
|
14
|
+
titleEn: string
|
|
15
|
+
contentPreset: SectionContentPresetId
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
open: boolean
|
|
20
|
+
onOpenChange: (open: boolean) => void
|
|
21
|
+
design: SectionCreateModalDesign
|
|
22
|
+
onCreate: (payload: SectionCreatePayload) => Promise<void>
|
|
23
|
+
creating?: boolean
|
|
24
|
+
shell: HostModuleContext['shell']
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SectionCreateModal({
|
|
28
|
+
open,
|
|
29
|
+
onOpenChange,
|
|
30
|
+
design,
|
|
31
|
+
onCreate,
|
|
32
|
+
creating = false,
|
|
33
|
+
shell,
|
|
34
|
+
}: Props) {
|
|
35
|
+
const {
|
|
36
|
+
Dialog,
|
|
37
|
+
DialogContent,
|
|
38
|
+
DialogDescription,
|
|
39
|
+
DialogFooter,
|
|
40
|
+
DialogHeader,
|
|
41
|
+
DialogTitle,
|
|
42
|
+
Button,
|
|
43
|
+
Input,
|
|
44
|
+
cn,
|
|
45
|
+
} = shell
|
|
46
|
+
|
|
47
|
+
const defaultTemplate =
|
|
48
|
+
design.templates.find((t) => t.id === design.defaultTemplateId) ?? design.templates[0]
|
|
49
|
+
|
|
50
|
+
const [selectedId, setSelectedId] = useState(defaultTemplate?.id ?? '')
|
|
51
|
+
const [titleFr, setTitleFr] = useState(defaultTemplate?.titleFr ?? '')
|
|
52
|
+
|
|
53
|
+
const selected = design.templates.find((t) => t.id === selectedId) ?? defaultTemplate
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!open || !defaultTemplate) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
setSelectedId(defaultTemplate.id)
|
|
60
|
+
setTitleFr(defaultTemplate.titleFr)
|
|
61
|
+
}, [open, defaultTemplate])
|
|
62
|
+
|
|
63
|
+
function selectTemplate(template: SectionTemplateDesign) {
|
|
64
|
+
setSelectedId(template.id)
|
|
65
|
+
setTitleFr(template.titleFr)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleSubmit() {
|
|
69
|
+
if (!selected || !titleFr.trim()) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
await onCreate({
|
|
73
|
+
titleFr: titleFr.trim(),
|
|
74
|
+
titleEn: selected.titleEn,
|
|
75
|
+
contentPreset: selected.contentPreset,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
81
|
+
<DialogContent className="flex max-h-[min(92vh,calc(100%-80px))] max-w-[min(96vw,920px)] flex-col gap-0 overflow-hidden p-0">
|
|
82
|
+
<DialogHeader className="shrink-0 px-[26px] pb-4 pt-6">
|
|
83
|
+
<DialogTitle className="font-heading text-[22px] font-semibold tracking-tight">
|
|
84
|
+
{design.titleFr}
|
|
85
|
+
</DialogTitle>
|
|
86
|
+
<DialogDescription className="text-[13px] leading-relaxed text-[var(--text-muted)]">
|
|
87
|
+
{design.subtitleFr}
|
|
88
|
+
</DialogDescription>
|
|
89
|
+
</DialogHeader>
|
|
90
|
+
|
|
91
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-[26px] pb-4">
|
|
92
|
+
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
|
|
93
|
+
{design.templates.map((template) => {
|
|
94
|
+
const active = template.id === selectedId
|
|
95
|
+
return (
|
|
96
|
+
<button
|
|
97
|
+
key={template.id}
|
|
98
|
+
type="button"
|
|
99
|
+
onClick={() => selectTemplate(template)}
|
|
100
|
+
className={cn(
|
|
101
|
+
'rounded-xl border px-3 py-3 text-left transition-colors',
|
|
102
|
+
active
|
|
103
|
+
? 'border-[var(--amber)] bg-[var(--color-amber-soft)] shadow-[inset_0_0_0_1px_rgba(245,158,11,0.35)]'
|
|
104
|
+
: 'border-[var(--border)] bg-white hover:border-[var(--border-md)] hover:bg-[var(--bg-muted)]/40',
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<span className="text-[22px] leading-none" aria-hidden>
|
|
108
|
+
{template.icon}
|
|
109
|
+
</span>
|
|
110
|
+
<p className="mt-2 font-heading text-[14px] font-semibold leading-snug text-[var(--text)]">
|
|
111
|
+
{template.titleFr}
|
|
112
|
+
</p>
|
|
113
|
+
<p className="mt-1 text-[12px] leading-snug text-[var(--text-muted)]">
|
|
114
|
+
{template.descriptionFr}
|
|
115
|
+
</p>
|
|
116
|
+
</button>
|
|
117
|
+
)
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="mt-5">
|
|
122
|
+
<label className="mb-1.5 block text-[13px] font-medium text-[var(--text)]">
|
|
123
|
+
{design.titleFieldLabelFr}
|
|
124
|
+
</label>
|
|
125
|
+
<Input
|
|
126
|
+
value={titleFr}
|
|
127
|
+
onChange={(e) => setTitleFr(e.target.value)}
|
|
128
|
+
placeholder={selected?.titleFr}
|
|
129
|
+
className="h-11"
|
|
130
|
+
/>
|
|
131
|
+
<p className="mt-1.5 text-[12px] text-[var(--text-muted)]">{design.titleFieldHintFr}</p>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<p className="mt-4 text-[12px] text-[var(--text-muted)]">{design.footerHintFr}</p>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<DialogFooter className="shrink-0 flex-row items-center justify-end gap-2 border-t border-[var(--border)] bg-[var(--bg-surface)] px-[26px] py-4">
|
|
138
|
+
<Button
|
|
139
|
+
type="button"
|
|
140
|
+
variant="ghost"
|
|
141
|
+
className="text-[var(--text-muted)]"
|
|
142
|
+
disabled={creating}
|
|
143
|
+
onClick={() => onOpenChange(false)}
|
|
144
|
+
>
|
|
145
|
+
{design.cancelLabelFr}
|
|
146
|
+
</Button>
|
|
147
|
+
<Button
|
|
148
|
+
type="button"
|
|
149
|
+
className="bg-[var(--amber)] font-semibold text-[var(--text)] hover:bg-[var(--amber-solid-hover)]"
|
|
150
|
+
disabled={creating || !titleFr.trim()}
|
|
151
|
+
onClick={() => void handleSubmit()}
|
|
152
|
+
>
|
|
153
|
+
{creating ? 'Création…' : design.submitLabelFr}
|
|
154
|
+
</Button>
|
|
155
|
+
</DialogFooter>
|
|
156
|
+
</DialogContent>
|
|
157
|
+
</Dialog>
|
|
158
|
+
)
|
|
159
|
+
}
|