wx-svelte-comments 2.0.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/license.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 XB Software Sp. z o.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "wx-svelte-comments",
3
+ "version": "2.0.1",
4
+ "description": "Simple Svelte component for adding a comments section on a page",
5
+ "productTag": "comments",
6
+ "productTrial": false,
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "vite build",
10
+ "build:dist": "vite build --mode dist",
11
+ "build:tests": "vite build --mode test",
12
+ "lint": "yarn eslint ./demos ./src",
13
+ "start": "vite --open",
14
+ "start:tests": "vite --open=/tests/ --host 0.0.0.0 --port 5100 --mode test",
15
+ "test": "true",
16
+ "test:cypress": "cypress run -P ./ --config \"baseUrl=http://localhost:5100/tests\""
17
+ },
18
+ "svelte": "src/index.js",
19
+ "exports": {
20
+ ".": {
21
+ "svelte": "./src/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/svar-widgets/comments.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://forum.svar.dev"
32
+ },
33
+ "homepage": "https://svar.dev/svelte/comments/",
34
+ "dependencies": {
35
+ "wx-comments-locales": "2.0.1",
36
+ "wx-lib-data-provider": "1.6.0",
37
+ "wx-lib-dom": "0.7.1",
38
+ "wx-lib-state": "1.9.0",
39
+ "wx-svelte-core": "2.0.1",
40
+ "wx-svelte-menu": "2.0.1"
41
+ },
42
+ "files": [
43
+ "src",
44
+ "readme.md",
45
+ "whatsnew.md",
46
+ "license.txt"
47
+ ],
48
+ "keywords": [
49
+ "svelte",
50
+ "comments",
51
+ "ui component",
52
+ "comments section"
53
+ ]
54
+ }
package/readme.md ADDED
@@ -0,0 +1,51 @@
1
+ <div align="center">
2
+
3
+ # SVAR Svelte Comments
4
+
5
+ [![npm](https://img.shields.io/npm/v/wx-svelte-comments.svg)](https://www.npmjs.com/package/wx-svelte-comments)
6
+ [![License](https://img.shields.io/github/license/svar-widgets/comments)](https://github.com/svar-widgets/comments/blob/main/license.txt)
7
+ [![npm downloads](https://img.shields.io/npm/dm/wx-svelte-comments.svg)](https://www.npmjs.com/package/wx-svelte-comments)
8
+
9
+ </div>
10
+
11
+ <div align="center">
12
+
13
+ [Documentation](https://docs.svar.dev/svelte/core/comments/) • [Demos](https://docs.svar.dev/svelte/core/samples-comments/#/base/willow)
14
+
15
+ </div>
16
+
17
+ A Svelte UI component for creating a comments section. Supports adding, editing, and deleting comments with ease.
18
+
19
+ ### How to Use
20
+
21
+ To use the widget, simply import the package and include the component in your Svelte file:
22
+
23
+ ```svelte
24
+ <script>
25
+ import { Comments } from "wx-svelte-comments";
26
+
27
+ const value = [];
28
+ </script>
29
+
30
+ <Comments {value} />
31
+ ```
32
+
33
+ ### How to Modify
34
+
35
+ Typically, you don't need to modify the code. However, if you wish to do so, follow these steps:
36
+
37
+ 1. Run `yarn` to install dependencies. Note that this project is a monorepo using `yarn` workspaces, so npm will not work
38
+ 2. Start the project in development mode with `yarn start`
39
+
40
+ ### Run Tests
41
+
42
+ To run the test:
43
+
44
+ 1. Start the test examples with:
45
+ ```sh
46
+ yarn start:tests
47
+ ```
48
+ 2. In a separate console, run the end-to-end tests with:
49
+ ```sh
50
+ yarn test:cypress
51
+ ```
@@ -0,0 +1,21 @@
1
+ <script>
2
+ import Layout from "./Layout.svelte";
3
+ import { Locale } from "wx-svelte-core";
4
+ import { en } from "wx-comments-locales";
5
+
6
+ const { ondata, onchange, value, ...props } = $props();
7
+ const finalData = $derived(ondata && value ? ondata(value) : value);
8
+
9
+ const handleOnchange = e => {
10
+ e.originalValue = value;
11
+ return onchange && onchange(e);
12
+ };
13
+ </script>
14
+
15
+ <Locale words={en} optional={true}>
16
+ {#await finalData}
17
+ <Layout data={[]} {...props} onchange={handleOnchange} />
18
+ {:then data}
19
+ <Layout {data} {...props} onchange={handleOnchange} />
20
+ {/await}
21
+ </Locale>
@@ -0,0 +1,212 @@
1
+ <script>
2
+ import { getContext, setContext } from "svelte";
3
+ import Messages from "./Messages.svelte";
4
+ import TextArea from "./TextArea.svelte";
5
+ import { ActionMenu } from "wx-svelte-menu";
6
+ import { uid } from "wx-lib-state";
7
+ import { dateToString } from "wx-lib-dom";
8
+
9
+ let {
10
+ onaction,
11
+ onchange,
12
+ readonly = false,
13
+ render = "flow",
14
+ format = "text",
15
+ users: rawUsers,
16
+ data: rawData,
17
+ activeUser,
18
+ focus = false,
19
+ } = $props();
20
+
21
+ let edit = $state(null);
22
+ const lang = getContext("wx-i18n");
23
+ const { calendar, comments, formats } = lang.getRaw();
24
+ const _ = lang.getGroup("comments");
25
+
26
+ const dateFormatter = dateToString(
27
+ comments.dateFormat || formats.dateFormat,
28
+ calendar
29
+ );
30
+ setContext("wx-comments-format", {
31
+ dateStr: date => dateFormatter(date),
32
+ });
33
+
34
+ const unknownUser = { id: 0, name: _("Unknown"), color: "hsl(0, 0%, 85%)" };
35
+
36
+ const users = $derived.by(() => {
37
+ if (!rawUsers) return [];
38
+ return rawUsers.map(u => {
39
+ if (!u.color)
40
+ return { ...u, color: "hsl(" + getColor(u.id + u.name) + ", 100%, 85%)" };
41
+ return u;
42
+ });
43
+ });
44
+
45
+ const author = $derived.by(() => {
46
+ if (typeof activeUser === "object") return activeUser;
47
+ const a = users.find(u => u.id === activeUser) || unknownUser;
48
+ if (a) return a;
49
+ return { id: activeUser || -1, name: _("Me"), color: "hsl(225, 76%, 67%)" };
50
+ });
51
+
52
+ const data = $derived.by(() => {
53
+ if (!rawData) return [];
54
+
55
+ // data with user objects
56
+ return rawData.map(d => {
57
+ if (typeof d.author === "object") return d;
58
+
59
+ const user = users ? users.find(u => u.id === d.user) : null;
60
+ return {
61
+ ...d,
62
+ author: user || unknownUser,
63
+ };
64
+ });
65
+ });
66
+
67
+ function getColor(name) {
68
+ let hash = 0;
69
+ for (let i = 0; i < name.length; i++) {
70
+ // eslint-disable-next-line no-bitwise
71
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
72
+ }
73
+ return (hash % 60) * 6;
74
+ }
75
+
76
+ function add(content) {
77
+ const comment = {
78
+ id: uid(),
79
+ content,
80
+ author,
81
+ user: author.id,
82
+ date: new Date(),
83
+ };
84
+
85
+ rawData = [...data, comment];
86
+ if (onchange){
87
+ const res = onchange({ value:rawData, comment, action: "add" });
88
+ if (res && typeof res === "object"){
89
+ if (res.then){
90
+ res.then((data) => {
91
+ updateAfter(comment.id, data);
92
+ });
93
+ } else {
94
+ updateAfter(comment.id, res);
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ function updateAfter(id, data){
101
+ const index = rawData.findIndex(d => d.id === id);
102
+
103
+ const copy = [ ...rawData ];
104
+ copy[index] = { ...rawData[index], ...data };
105
+ rawData = copy;
106
+ }
107
+
108
+ function remove(id) {
109
+ if (edit === id) edit = null;
110
+
111
+ rawData = rawData.filter(d => d.id !== id);
112
+ onchange && onchange({ value: rawData, id, action: "delete" });
113
+ }
114
+
115
+ function update(id, content) {
116
+ let comment;
117
+ rawData = rawData.map(d => {
118
+ if (d.id === id) {
119
+ comment = { ...d, content };
120
+ return comment;
121
+ } else return d;
122
+ });
123
+ edit = null;
124
+
125
+ onchange && onchange({ value: rawData, id, action: "update", comment });
126
+ }
127
+
128
+ function cancelUpdate() {
129
+ edit = null;
130
+ }
131
+
132
+ function handleActionMenu(ev) {
133
+ const { context, action } = ev;
134
+
135
+ if (!action) return;
136
+
137
+ onaction && onaction({ action: "menu-clicked" });
138
+
139
+ switch (action.id) {
140
+ case "edit-comment":
141
+ edit = context;
142
+ break;
143
+ case "delete-comment":
144
+ remove(context);
145
+ break;
146
+ }
147
+ }
148
+
149
+ const menuItems = [
150
+ {
151
+ id: "edit-comment",
152
+ text: _("Edit"),
153
+ icon: "wxi-edit-outline",
154
+ },
155
+ {
156
+ id: "delete-comment",
157
+ text: _("Delete"),
158
+ icon: "wxi-delete-outline",
159
+ },
160
+ ];
161
+
162
+ let menu = $state(null);
163
+ </script>
164
+
165
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
166
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
167
+ <div class="wx-comments-list">
168
+ <ActionMenu
169
+ at={"bottom"}
170
+ bind:this={menu}
171
+ options={menuItems}
172
+ resolver={item => item}
173
+ dataKey="commentMenuId"
174
+ onclick={handleActionMenu}
175
+ />
176
+ <div class="wx-list" onclick={menu.show}>
177
+ <Messages
178
+ {author}
179
+ {render}
180
+ {data}
181
+ {format}
182
+ {edit}
183
+ onpost={ev => update(edit, ev.value)}
184
+ oncancel={cancelUpdate}
185
+ ></Messages>
186
+ </div>
187
+ {#if !readonly && !edit}
188
+ <TextArea
189
+ {author}
190
+ flow={render === "flow"}
191
+ {focus}
192
+ onpost={ev => add(ev.value)}
193
+ buttonLabel={_("Add")}
194
+ />
195
+ {/if}
196
+ </div>
197
+
198
+ <style>
199
+ .wx-comments-list {
200
+ height: 100%;
201
+ width: 100%;
202
+ display: flex;
203
+ flex-direction: column;
204
+ }
205
+ .wx-list {
206
+ display: flex;
207
+ flex-direction: column;
208
+ margin-bottom: 4px;
209
+ overflow-y: auto;
210
+ flex: 1;
211
+ }
212
+ </style>
@@ -0,0 +1,135 @@
1
+ <script>
2
+ import { getContext } from "svelte";
3
+ import UserIcon from "./UserIcon.svelte";
4
+
5
+ const {
6
+ owned,
7
+ author,
8
+ date,
9
+ edit,
10
+ children,
11
+ } = $props();
12
+
13
+ const dateFormatter = getContext("wx-comments-format").dateStr;
14
+ </script>
15
+
16
+ <div class="wx-bubble" class:wx-owned={owned}>
17
+ <div class="wx-bubble-wrapper">
18
+ <div class="wx-avatar">
19
+ <UserIcon data={author} />
20
+ </div>
21
+ <div class="wx-main-bubble">
22
+ <span class="wx-author-name">{author.name}</span>
23
+ {#if owned}
24
+ <div class="wx-agent-message">
25
+ <div class="wx-message">
26
+ {@render children()}
27
+ {#if edit !== owned}<div class="wx-comment-date">
28
+ {dateFormatter(date)}
29
+ </div>{/if}
30
+ </div>
31
+ <div class="wx-menu-icon" data-comment-menu-id={owned}>
32
+ <i class="wx-icon wxi-dots-v"></i>
33
+ </div>
34
+ </div>
35
+ {:else}
36
+ <div class="wx-message">
37
+ {@render children()}
38
+ <div class="wx-comment-date">{dateFormatter(date)}</div>
39
+ </div>
40
+ {/if}
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <style>
46
+ .wx-bubble {
47
+ width: 100%;
48
+ display: flex;
49
+ justify-content: end;
50
+ }
51
+ .wx-bubble.wx-owned {
52
+ justify-content: start;
53
+ }
54
+ .wx-bubble-wrapper {
55
+ width: 65%;
56
+ display: flex;
57
+ justify-content: end;
58
+ gap: 8px;
59
+ }
60
+ .wx-owned .wx-bubble-wrapper {
61
+ width: calc(65% + 28px);
62
+ justify-content: start;
63
+ }
64
+ .wx-author-name {
65
+ margin-left: auto;
66
+ }
67
+ .wx-owned .wx-author-name {
68
+ margin-left: 0;
69
+ }
70
+ .wx-message {
71
+ background-color: var(--wx-comments-msg-background);
72
+ padding: 8px 12px;
73
+ white-space: pre-wrap;
74
+ line-height: 24px;
75
+ border-radius: 6px 0 6px 6px;
76
+ width: 100%;
77
+ display: flex;
78
+ flex-direction: column;
79
+ }
80
+ .wx-owned .wx-message {
81
+ background-color: var(--wx-comments-msg-background-agent);
82
+ border-radius: 0 6px 6px 6px;
83
+ width: 100%;
84
+ }
85
+ .wx-author-name {
86
+ font-weight: 600;
87
+ font-size: 14px;
88
+ line-height: 24px;
89
+ }
90
+ .wx-agent-message {
91
+ display: flex;
92
+ align-items: flex-end;
93
+ gap: 4px;
94
+ cursor: pointer;
95
+ }
96
+ .wx-menu-icon {
97
+ opacity: 0;
98
+ height: 24px;
99
+ color: #9fa1ae;
100
+ cursor: pointer;
101
+ height: 100%;
102
+ }
103
+ .wx-menu-icon i {
104
+ font-size: 24px;
105
+ line-height: 24px;
106
+ }
107
+ .wx-menu-icon:hover {
108
+ color: var(--wx-color-primary);
109
+ }
110
+ .wx-agent-message:hover .wx-menu-icon {
111
+ opacity: 1;
112
+ transition: all 0.2s linear;
113
+ }
114
+ .wx-main-bubble {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 4px;
118
+ width: 100%;
119
+ }
120
+ .wx-avatar {
121
+ height: 32px;
122
+ width: 32px;
123
+ }
124
+
125
+ .wx-bubble:not(.wx-owned) .wx-bubble-wrapper {
126
+ flex-direction: row-reverse;
127
+ }
128
+
129
+ .wx-comment-date {
130
+ font-size: var(--wx-font-size-sm);
131
+ color: var(--wx-color-font-alt);
132
+ display: flex;
133
+ flex-direction: row-reverse;
134
+ }
135
+ </style>
@@ -0,0 +1,27 @@
1
+ <script>
2
+ import TextArea from "../TextArea.svelte";
3
+ import { messages, formats } from "./index.js";
4
+
5
+ const {
6
+ text,
7
+ date,
8
+ owned,
9
+ render,
10
+ format = "text",
11
+ author,
12
+ edit,
13
+ onpost,
14
+ oncancel
15
+ } = $props();
16
+
17
+ const BoxRender = typeof render === "string" ? messages[render] : render;
18
+ const TextRender = typeof format === "string" ? formats[format] : format;
19
+ </script>
20
+
21
+ <BoxRender {owned} {edit} {author} {date}>
22
+ {#if edit && edit === owned}
23
+ <TextArea focus={true} {onpost} {oncancel} value={text} />
24
+ {:else}
25
+ <TextRender {text} {edit} />
26
+ {/if}
27
+ </BoxRender>
@@ -0,0 +1,79 @@
1
+ <script>
2
+ import { getContext } from "svelte";
3
+ import UserIcon from "./UserIcon.svelte";
4
+
5
+ const {
6
+ owned,
7
+ author,
8
+ date,
9
+ edit,
10
+ children,
11
+ } = $props();
12
+
13
+ const dateFormatter = getContext("wx-comments-format").dateStr;
14
+ </script>
15
+
16
+ <div class="wx-flow" class:owned>
17
+ <div class="wx-flow-toolbar">
18
+ <UserIcon data={author} />
19
+ <span class="wx-author-name">{author.name}</span>
20
+ {#if owned && owned !== edit}
21
+ <div class="wx-menu-icon" data-comment-menu-id={owned}>
22
+ <i class="wx-icon wxi-dots-v" ></i>
23
+ </div>
24
+ {/if}
25
+ </div>
26
+ <div class="wx-comment-date">{dateFormatter(date)}</div>
27
+ <div class="wx-message">
28
+ {@render children()}
29
+ </div>
30
+ </div>
31
+
32
+ <style>
33
+ .wx-flow {
34
+ width: 100%;
35
+ padding: 16px 20px;
36
+ border-radius: 6px;
37
+ }
38
+ .wx-flow.owned {
39
+ background-color: var(--wx-comments-msg-background-agent);
40
+ }
41
+ .wx-flow:hover .wx-menu-icon {
42
+ opacity: 1;
43
+ transition: all 0.2s linear;
44
+ }
45
+ .wx-flow-toolbar {
46
+ width: 100%;
47
+ display: flex;
48
+ gap: 8px;
49
+ }
50
+ .wx-message {
51
+ margin-left: 32px;
52
+ line-height: 24px;
53
+ white-space: pre-wrap;
54
+ }
55
+ .wx-author-name {
56
+ font-weight: 600;
57
+ font-size: 14px;
58
+ line-height: 24px;
59
+ }
60
+ .wx-menu-icon {
61
+ color: #9fa1ae;
62
+ cursor: pointer;
63
+ margin-left: auto;
64
+ opacity: 0;
65
+ height: 24px;
66
+ }
67
+ .wx-menu-icon:hover {
68
+ color: var(--wx-color-primary);
69
+ }
70
+ .wx-menu-icon i {
71
+ font-size: 24px;
72
+ line-height: 24px;
73
+ }
74
+ .wx-comment-date {
75
+ margin-left: 32px;
76
+ font-size: var(--wx-font-size-sm);
77
+ color: var(--wx-color-font-alt);
78
+ }
79
+ </style>
@@ -0,0 +1,7 @@
1
+ <script>
2
+ import { html } from "../../libs/lima.es.js";
3
+ export let text;
4
+ </script>
5
+
6
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
7
+ {@html html(text)}
@@ -0,0 +1,5 @@
1
+ <script>
2
+ export let text;
3
+ </script>
4
+
5
+ {text}
@@ -0,0 +1,77 @@
1
+ <script>
2
+ import { getContrastingColor } from "../../helpers/colors";
3
+
4
+ const {
5
+ data = {
6
+ name: "",
7
+ avatar: "",
8
+ color: "",
9
+ },
10
+ noTransform = false,
11
+ size = "small",
12
+ border = true,
13
+ } = $props();
14
+
15
+ const firstLetters = $derived(data.name
16
+ .split(" ")
17
+ .map(name => name[0])
18
+ .join(""));
19
+
20
+ let style = $derived(data.color ? `background: ${data.color};` : "");
21
+ let css = $derived(data.color ? `wx-comments-avatar-color-${getContrastingColor(data.color)}` : "");
22
+ </script>
23
+
24
+ <div class="wx-user wx-{size} {css}" class:wx-border={border} {style}>
25
+ {#if data.avatar}
26
+ <img src={data.avatar} alt={data.name} />
27
+ {:else if noTransform}{data.name}{:else}{firstLetters}{/if}
28
+ </div>
29
+
30
+ <style>
31
+ .wx-user {
32
+ --wx-comments-user-icon-size: 36px;
33
+
34
+ font-size: 12px;
35
+ font-weight: 500;
36
+ line-height: 12px;
37
+ border-radius: 50%;
38
+ color: var(--wx-color-font);
39
+ display: flex;
40
+ justify-content: center;
41
+ align-items: center;
42
+ overflow: hidden;
43
+ background-color: #ca9cec;
44
+ color: var(--wx-color-font);
45
+ }
46
+
47
+ .wx-user.wx-normal {
48
+ width: var(--wx-comments-user-icon-size);
49
+ height: var(--wx-comments-user-icon-size);
50
+ font-size: var(--wx-font-size);
51
+ }
52
+
53
+ .wx-user.wx-small {
54
+ width: 24px;
55
+ height: 24px;
56
+ font-size: var(--wx-font-size-sm);
57
+ }
58
+
59
+ .wx-user img {
60
+ display: block;
61
+ width: 100%;
62
+ height: 100%;
63
+ object-fit: cover;
64
+ }
65
+
66
+ .wx-user:not(:first-child) {
67
+ margin-left: -15px;
68
+ }
69
+
70
+ .wx-user.wx-comments-avatar-color-light {
71
+ color: var(--wx-color-primary-font);
72
+ }
73
+
74
+ .wx-user.wx-comments-avatar-color-dark {
75
+ color: var(--wx-avatar-color-dark);
76
+ }
77
+ </style>
@@ -0,0 +1,14 @@
1
+ import Bubbles from "./Bubbles.svelte";
2
+ import Flow from "./Flow.svelte";
3
+
4
+ import MarkdownPlain from "./MarkdownPlain.svelte";
5
+ import TextPlain from "./TextPlain.svelte";
6
+
7
+ export const messages = {
8
+ bubbles: Bubbles,
9
+ flow: Flow,
10
+ };
11
+ export const formats = {
12
+ markdown: MarkdownPlain,
13
+ text: TextPlain,
14
+ };
@@ -0,0 +1,51 @@
1
+ <script>
2
+ import Message from "./Messages/Common.svelte";
3
+
4
+ const {
5
+ data,
6
+ render = "blocks",
7
+ format,
8
+ author,
9
+ edit,
10
+ onpost,
11
+ oncancel,
12
+ } = $props();
13
+
14
+ const css = $derived(typeof render === "string" ? render : "blocks");
15
+ </script>
16
+
17
+ <div class="wx-messages wx-{css}">
18
+ {#each data as message (message.id)}
19
+ <Message
20
+ text={message.content}
21
+ date={message.date}
22
+ author={message.author}
23
+ owned={message.author.id === author.id ? message.id : null}
24
+ {render}
25
+ {edit}
26
+ format={message.format || format}
27
+ {onpost}
28
+ {oncancel}
29
+ />
30
+ {/each}
31
+ </div>
32
+
33
+ <style>
34
+ .wx-messages {
35
+ padding: 10px 0;
36
+ display: flex;
37
+ flex-direction: column;
38
+ }
39
+
40
+ .wx-messages.wx-bubbles {
41
+ gap: 24px;
42
+ }
43
+
44
+ .wx-messages.wx-cards {
45
+ gap: 16px;
46
+ }
47
+
48
+ .wx-messages.wx-blocks {
49
+ gap: 24px;
50
+ }
51
+ </style>
@@ -0,0 +1,81 @@
1
+ <script>
2
+ import { onMount, getContext } from "svelte";
3
+ import { TextArea, Button } from "wx-svelte-core";
4
+ import UserIcon from "./Messages/UserIcon.svelte";
5
+
6
+ let {
7
+ focus = false,
8
+ buttonLabel = "Send",
9
+ flow = false,
10
+ value = $bindable(""),
11
+ author,
12
+ onpost,
13
+ } = $props();
14
+
15
+ let textarea, area;
16
+
17
+ const _ = getContext("wx-i18n").getGroup("comments");
18
+
19
+ onMount(() => {
20
+ area = textarea.querySelector("textarea");
21
+ if (focus) area.focus();
22
+ area.onkeydown = ev => {
23
+ if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey)) {
24
+ ev.preventDefault();
25
+ onclick();
26
+ }
27
+ };
28
+ });
29
+
30
+ function onclick() {
31
+ if (!value) return;
32
+ onpost && onpost({ value });
33
+ value = "";
34
+ area.focus();
35
+ }
36
+ </script>
37
+
38
+ <div
39
+ class="wx-comments-textarea"
40
+ class:wx-flow={flow}
41
+ data-focus="yes"
42
+ bind:this={textarea}
43
+ >
44
+ <div class="wx-textarea-wrapper">
45
+ {#if author}
46
+ <div class="wx-textarea-avatar">
47
+ <UserIcon data={author} />
48
+ </div>
49
+ {/if}
50
+ <TextArea placeholder={_("Add a comment...")} bind:value />
51
+ </div>
52
+ <div class="wx-textarea-bottombar">
53
+ <Button type="primary" {onclick}>{buttonLabel}</Button>
54
+ </div>
55
+ </div>
56
+
57
+ <style>
58
+ .wx-comments-textarea {
59
+ width: 100%;
60
+ background-color: inherit;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 12px;
64
+ }
65
+ .wx-textarea-wrapper {
66
+ position: relative;
67
+ display: flex;
68
+ gap: 8px;
69
+ }
70
+ .wx-comments-textarea.wx-flow .wx-textarea-avatar {
71
+ margin-left: 20px;
72
+ }
73
+ .wx-textarea-bottombar {
74
+ text-align: right;
75
+ }
76
+ .wx-textarea-bottombar :global(button) {
77
+ padding: 6px 16px;
78
+ font-weight: 600;
79
+ line-height: 20px;
80
+ }
81
+ </style>
@@ -0,0 +1,58 @@
1
+ export function getContrastingColor(color) {
2
+ if (color.startsWith("hsl")) {
3
+ const n = toHSLArray(color);
4
+ return n[2] > 55 ? "dark" : "light";
5
+ }
6
+
7
+ if (color.startsWith("#") || color.startsWith("rgb")) {
8
+ const rgb = hexToRgb(color, true);
9
+ const brightness = Math.round(
10
+ (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000
11
+ );
12
+ return brightness > 180 ? "dark" : "light";
13
+ }
14
+
15
+ // consider it's an html color, most of them are vivid, so
16
+ return "light";
17
+ }
18
+
19
+ export function toHSLArray(hslStr) {
20
+ return hslStr.match(/\d+/g).map(Number);
21
+ }
22
+
23
+ export function hexToRgb(hex, asObject = false) {
24
+ if (hex.startsWith("rgb")) {
25
+ return asObject ? rgbStringToObj(hex) : hex;
26
+ }
27
+ let r = 0,
28
+ g = 0,
29
+ b = 0;
30
+
31
+ // 3 digits
32
+ if (hex.length == 4) {
33
+ r = "0x" + hex[1] + hex[1];
34
+ g = "0x" + hex[2] + hex[2];
35
+ b = "0x" + hex[3] + hex[3];
36
+
37
+ // 6 digits
38
+ } else if (hex.length == 7) {
39
+ r = "0x" + hex[1] + hex[2];
40
+ g = "0x" + hex[3] + hex[4];
41
+ b = "0x" + hex[5] + hex[6];
42
+ }
43
+ r = +r;
44
+ g = +g;
45
+ b = +b;
46
+ return asObject ? { r, g, b } : "rgb(" + r + "," + g + "," + b + ")";
47
+ }
48
+
49
+ export function rgbStringToObj(rgb) {
50
+ const sep = rgb.indexOf(",") > -1 ? "," : " ";
51
+ // Turn "rgb(r,g,b)" into [r,g,b]
52
+ const arr = rgb.substring(4).split(")")[0].split(sep);
53
+
54
+ const r = (+arr[0]).toString(16),
55
+ g = (+arr[1]).toString(16),
56
+ b = (+arr[2]).toString(16);
57
+ return { r: +r, g: +g, b: +b };
58
+ }
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import Comments from "./components/Comments.svelte";
2
+ import Willow from "./themes/Willow.svelte";
3
+ import WillowDark from "./themes/WillowDark.svelte";
4
+
5
+ export { Comments, Willow, WillowDark };
@@ -0,0 +1,337 @@
1
+ function parse(str, ast, notFinal) {
2
+ let content;
3
+ let ctype;
4
+ let headerlevel, newLineCounter;
5
+ let checkbehind, unsurepoint;
6
+ let linestart;
7
+ let i;
8
+ if (!ast) {
9
+ ast = {
10
+ nodes: [{ d: [], t: "p", f: false, i: 0 }],
11
+ length: 0,
12
+ state: {},
13
+ };
14
+ linestart = 0;
15
+ headerlevel = 0;
16
+ checkbehind = -1;
17
+ unsurepoint = -1;
18
+ content = "";
19
+ ctype = "";
20
+ newLineCounter = 0;
21
+ i = 0;
22
+ } else {
23
+ ({
24
+ linestart,
25
+ headerlevel,
26
+ checkbehind,
27
+ unsurepoint,
28
+ content,
29
+ ctype,
30
+ newLineCounter,
31
+ i,
32
+ } = ast.state);
33
+ }
34
+ if (unsurepoint) {
35
+ const b = ast.nodes[ast.nodes.length - 1];
36
+ if (b.d.length) {
37
+ ast.length -= b.d[b.d.length - 1].c.length;
38
+ b.d.pop();
39
+ }
40
+ }
41
+ const finishBlock = notFinal2 => {
42
+ if (content !== "") {
43
+ finishText("", notFinal2);
44
+ }
45
+ const lastBlock = ast.nodes[ast.nodes.length - 1];
46
+ lastBlock.f = notFinal2 !== 1;
47
+ lastBlock.i = i;
48
+ if (headerlevel > 0) {
49
+ lastBlock.t = `h${headerlevel}`;
50
+ headerlevel = -1;
51
+ }
52
+ if (notFinal2 === 0) ast.nodes.push({ d: [], t: "p", f: false, i: 0 });
53
+ };
54
+ const finishText = (t, notFinal2 = 2) => {
55
+ const block = ast.nodes[ast.nodes.length - 1];
56
+ const last = block.d[block.d.length - 1];
57
+ const utype = notFinal2 === 2 ? ctype : "";
58
+ if (last && last.t === utype && unsurepoint < 0) {
59
+ const text = last;
60
+ ast.length += content.length;
61
+ text.c = text.c + content;
62
+ text.i = i;
63
+ } else {
64
+ const c =
65
+ utype === "" && unsurepoint >= 0
66
+ ? str.substring(unsurepoint)
67
+ : content;
68
+ ast.length += c.length;
69
+ block.d.push({ c, t: utype, f: notFinal2 !== 1, i });
70
+ }
71
+ if (notFinal2 === 2) {
72
+ if (t) {
73
+ unsurepoint = checkbehind;
74
+ } else {
75
+ unsurepoint = -1;
76
+ }
77
+ }
78
+ if (notFinal2 !== 1) {
79
+ content = "";
80
+ ctype = t;
81
+ checkbehind = -1;
82
+ }
83
+ };
84
+ for (; i < str.length; i++) {
85
+ const char = str[i];
86
+ if (char === "\n") {
87
+ newLineCounter++;
88
+ linestart = i + 1;
89
+ continue;
90
+ }
91
+ if (char === " " || char === " ") {
92
+ if (newLineCounter > 0) {
93
+ continue;
94
+ }
95
+ }
96
+ if (newLineCounter > 0) {
97
+ if (newLineCounter > 1) {
98
+ finishBlock(0);
99
+ } else {
100
+ content += "\n";
101
+ }
102
+ newLineCounter = 0;
103
+ }
104
+ if (char == "*" || char == "`" || char == "#") {
105
+ if (str[i - 1] == "\\") {
106
+ content += char;
107
+ } else if (checkbehind === -1) {
108
+ checkbehind = i;
109
+ }
110
+ } else {
111
+ if (checkbehind >= 0) {
112
+ const l = i - checkbehind;
113
+ if (str[i - 1] == "*") {
114
+ if (l === 2) {
115
+ if (ctype === "strong") {
116
+ finishText("");
117
+ } else {
118
+ finishText("strong");
119
+ }
120
+ } else if (l === 1) {
121
+ if (ctype === "em") {
122
+ finishText("");
123
+ } else {
124
+ finishText("em");
125
+ }
126
+ }
127
+ checkbehind = -1;
128
+ } else if (str[i - 1] == "`" && l == 1) {
129
+ if (ctype === "code") {
130
+ finishText("");
131
+ } else {
132
+ finishText("code");
133
+ }
134
+ checkbehind = -1;
135
+ } else if (
136
+ str[i - 1] == "#" &&
137
+ str[i] === " " &&
138
+ checkbehind === linestart &&
139
+ i - checkbehind <= 6
140
+ ) {
141
+ headerlevel = i - checkbehind;
142
+ checkbehind = -1;
143
+ continue;
144
+ }
145
+ }
146
+ if (checkbehind !== -1) {
147
+ content += str.substring(checkbehind, i);
148
+ checkbehind = -1;
149
+ }
150
+ content += char;
151
+ }
152
+ }
153
+ finishBlock(notFinal ? 1 : -1);
154
+ if (notFinal) {
155
+ ast.state = {
156
+ linestart,
157
+ headerlevel,
158
+ checkbehind,
159
+ unsurepoint,
160
+ content,
161
+ ctype,
162
+ newLineCounter,
163
+ i,
164
+ };
165
+ } else {
166
+ ast.state = {};
167
+ }
168
+ return ast;
169
+ }
170
+ function render(target, ast) {
171
+ _renderNodes(target, ast.nodes);
172
+ }
173
+ function _renderNodes(target, nodes) {
174
+ if (!target) return;
175
+ for (const node of nodes) {
176
+ if (typeof node.d !== "undefined") {
177
+ const block = node;
178
+ const el = document.createElement(block.t);
179
+ target.appendChild(el);
180
+ _renderNodes(el, block.d);
181
+ } else {
182
+ const text = node;
183
+ if (text.t) {
184
+ const el = document.createElement(node.t);
185
+ el.textContent = text.c;
186
+ target.appendChild(el);
187
+ } else {
188
+ const el = document.createTextNode(text.c);
189
+ target.appendChild(el);
190
+ }
191
+ }
192
+ }
193
+ }
194
+ function renderNext(target, ast, prevState, distance, cursor) {
195
+ if (!target) return;
196
+ return (
197
+ _renderNodesNext(target, ast.nodes, prevState, distance, 0, cursor) ===
198
+ 0
199
+ );
200
+ }
201
+ function _renderNodesNext(target, nodes, prevState, distance, level, cursor) {
202
+ let start = 0,
203
+ textIndex = 0,
204
+ el = null,
205
+ temp = null;
206
+ const prev = prevState[level];
207
+ if (prev) {
208
+ start = prev.nodeIndex;
209
+ textIndex = prev.textIndex;
210
+ el = prev.target;
211
+ temp = prev.temp;
212
+ }
213
+ for (let i = start; i < nodes.length; i++) {
214
+ const node = nodes[i];
215
+ if (typeof node.d !== "undefined") {
216
+ const block = nodes[i];
217
+ if (!el) {
218
+ el = document.createElement(block.t);
219
+ target.appendChild(el);
220
+ }
221
+ distance = _renderNodesNext(
222
+ el,
223
+ block.d,
224
+ prevState,
225
+ distance,
226
+ level + 1,
227
+ cursor
228
+ );
229
+ if (!distance) {
230
+ prevState[level] = { nodeIndex: i, textIndex: 0, target: el };
231
+ return 0;
232
+ }
233
+ el = null;
234
+ } else {
235
+ let textContent;
236
+ const text = node;
237
+ if (text.c.length - textIndex > distance) {
238
+ textContent = text.c.substring(textIndex, textIndex + distance);
239
+ textIndex += distance;
240
+ distance = 0;
241
+ } else {
242
+ textContent = text.c.substring(textIndex);
243
+ distance -= text.c.length - textIndex;
244
+ textIndex = 0;
245
+ }
246
+ if (text.t) {
247
+ if (!el) {
248
+ el = document.createElement(text.t);
249
+ target.insertBefore(el, temp);
250
+ el.textContent = textContent;
251
+ } else {
252
+ el.textContent += textContent;
253
+ }
254
+ } else {
255
+ if (!el) {
256
+ const el2 = document.createTextNode(textContent);
257
+ target.insertBefore(el2, temp);
258
+ } else {
259
+ el.textContent += textContent;
260
+ }
261
+ }
262
+ if (distance === 0) {
263
+ if (!temp) temp = addCursor(target, cursor);
264
+ prevState[level] = textIndex
265
+ ? { temp, nodeIndex: i, textIndex, target: el }
266
+ : { temp, nodeIndex: i + 1, textIndex: 0, target: null };
267
+ return distance;
268
+ }
269
+ el = null;
270
+ }
271
+ }
272
+ prevState[level] = null;
273
+ if (temp) temp.parentNode.removeChild(temp);
274
+ return distance;
275
+ }
276
+ function addCursor(parent, name) {
277
+ const el = document.createElement("div");
278
+ el.className = name;
279
+ el.textContent = " ";
280
+ parent.appendChild(el);
281
+ return el;
282
+ }
283
+ function asHTML(a) {
284
+ let out = "";
285
+ for (const node of a) {
286
+ if (typeof node.d !== "undefined") {
287
+ out += "<" + node.t + ">";
288
+ out += asHTML(node.d);
289
+ out += "</" + node.t + ">";
290
+ } else {
291
+ if (node.t) out += "<" + node.t + ">";
292
+ out += node.c;
293
+ if (node.t) out += "</" + node.t + ">";
294
+ }
295
+ }
296
+ return out;
297
+ }
298
+ function asMarkdown(a) {
299
+ let out = "";
300
+ let space = "";
301
+ for (const node of a) {
302
+ if (typeof node.d !== "undefined") {
303
+ if (node.t === "h1") {
304
+ out += space + "# ";
305
+ } else if (node.t === "h2") {
306
+ out += space + "## ";
307
+ } else if (node.t === "h3") {
308
+ out += space + "### ";
309
+ } else if (node.t === "h4") {
310
+ out += space + "#### ";
311
+ } else if (node.t === "h5") {
312
+ out += space + "##### ";
313
+ } else if (node.t === "h6") {
314
+ out += space + "###### ";
315
+ } else {
316
+ out += space;
317
+ }
318
+ space = "\n\n";
319
+ out += asMarkdown(node.d);
320
+ } else {
321
+ let wrap = "";
322
+ if (node.t === "strong") {
323
+ wrap = "**";
324
+ } else if (node.t === "em") {
325
+ wrap = "*";
326
+ } else if (node.t === "code") {
327
+ wrap = "`";
328
+ }
329
+ out += wrap + node.c + wrap;
330
+ }
331
+ }
332
+ return out;
333
+ }
334
+ function html(s) {
335
+ return asHTML(parse(s).nodes);
336
+ }
337
+ export { asHTML, asMarkdown, html, parse, render, renderNext };
@@ -0,0 +1,58 @@
1
+ <script>
2
+ import { Willow } from "wx-svelte-core";
3
+
4
+ const { children, fonts } = $$props;
5
+ </script>
6
+
7
+ {#if children}
8
+ <Willow {fonts}>
9
+ {@render children()}
10
+ </Willow>
11
+ {:else}
12
+ <Willow {fonts} />
13
+ {/if}
14
+
15
+ <style>
16
+ :global(.wx-willow-theme) {
17
+ --wx-theme-name: willow;
18
+
19
+ --wx-comments-background: #ffffff;
20
+ --wx-comments-line-height: 24px;
21
+ --wx-comments-border: 1px solid #e6e6e6;
22
+
23
+ --wx-comments-msg-background: #d5eaf7;
24
+ --wx-comments-msg-background-agent: #f4f5f9;
25
+ --wx-comments-msg-background-hover: rgba(57, 169, 239, 0.03);
26
+ --wx-comments-avatar-icon-color: #ffffff;
27
+ --wx-comments-msg-blocks-background: #e6e6e6;
28
+ --wx-comments-msg-blocks-background-agent: #ffffff;
29
+ --wx-comments-msg-controls-shadow: 0px 3px 10px 0px
30
+ rgba(44, 47, 60, 0.12),
31
+ 0px 1px 2px 0px rgba(44, 47, 60, 0.06);
32
+
33
+ --wx-comments-sidebar-background: #fafafc;
34
+ --wx-comments-sidebar-chat-hover: #eaedf5;
35
+
36
+ --wx-comments-popup-agent-hover: #f7f7f7;
37
+ --wx-comments-popup-border: 1px solid #ffffff;
38
+ --wx-comments-popup-background: #ffffff;
39
+
40
+ --wx-comments-textarea-border: var(--wx-comments-border);
41
+ --wx-comments-textarea-focus-border: 1px solid var(--wx-color-primary);
42
+ --wx-comments-textarea-color: #475466;
43
+
44
+ --wx-comments-button-icon--color-disabled: #9fa1ae;
45
+
46
+ --wx-comments-scroll-background: #e6e6e6;
47
+ --wx-comments-slider-background: #f2f3f7;
48
+
49
+ --ww-chat-modal-background: #ffffff;
50
+
51
+ --wx-comments-header-background: rgba(122, 103, 235, 0.2);
52
+ --wx-comments-icon-color: #9fa1ae;
53
+
54
+ --wx-comments-button-toggle-sidebar-background: #f2f3f7;
55
+
56
+ --wx-avatar-color-dark: #2a2b2d;
57
+ }
58
+ </style>
@@ -0,0 +1,56 @@
1
+ <script>
2
+ import { WillowDark } from "wx-svelte-core";
3
+
4
+ const { children, fonts } = $$props;
5
+ </script>
6
+
7
+ {#if children}
8
+ <WillowDark {fonts}>
9
+ {@render children()}
10
+ </WillowDark>
11
+ {:else}
12
+ <WillowDark {fonts} />
13
+ {/if}
14
+
15
+ <style>
16
+ :global(.wx-willow-dark-theme) {
17
+ --wx-theme-name: willow-dark;
18
+
19
+ --wx-comments-background: #2b343b;
20
+ --wx-comments-line-height: 24px;
21
+ --wx-comments-border: 1px solid #384047;
22
+
23
+ --wx-comments-msg-background: rgba(122, 102, 235, 0.2);
24
+ --wx-comments-msg-background-agent: #384047;
25
+ --wx-comments-msg-background-hover: rgb(122, 103, 235, 0.1);
26
+ --wx-comments-avatar-icon-color: #2b343b;
27
+ --wx-comments-msg-blocks-background: #384047;
28
+ --wx-comments-msg-blocks-background-agent: #20262b;
29
+ --wx-comments-msg-controls-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.16);
30
+
31
+ --wx-comments-sidebar-background: #2a2b2d;
32
+ --wx-comments-sidebar-chat-hover: #384047;
33
+
34
+ --wx-comments-popup-agent-hover: #384047;
35
+ --wx-comments-popup-border: var(--wx-comments-border);
36
+ --wx-comments-popup-background: #2a2b2d;
37
+
38
+ --wx-comments-textarea-border: var(--wx-comments-border);
39
+ --wx-comments-textarea-focus-border: 1px solid var(--wx-color-primary);
40
+ --wx-comments-textarea-color: rgba(255, 255, 255, 0.9);
41
+
42
+ --wx-comments-button-icon--color-disabled: #20262b;
43
+
44
+ --wx-comments-scroll-background: #20262b;
45
+ --wx-comments-slider-background: #384047;
46
+
47
+ --ww-chat-modal-background: #2b343b;
48
+
49
+ --wx-comments-header-background: #2a2b2d;
50
+ --wx-comments-icon-color: #9fa1ae;
51
+
52
+ --wx-comments-button-toggle-sidebar-background: #384047;
53
+
54
+ --wx-avatar-color-dark: #2a2b2d;
55
+ }
56
+ </style>
package/whatsnew.md ADDED
@@ -0,0 +1,15 @@
1
+ ### 2.0.0
2
+
3
+ - public release
4
+
5
+ ### 0.2.1
6
+
7
+ - [update] autofocus is replaced by `focus` property
8
+
9
+ ### 0.2.0
10
+
11
+ - [update] data sync api
12
+
13
+ ### 0.1.0
14
+
15
+ - initial version