@nectary/labs 2.1.1 → 2.1.2
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/color-select.d.ts +32 -0
- package/color-select.js +79 -0
- package/imports.d.ts +9 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +7 -2
- package/phone-preview-rcs-channel.d.ts +47 -0
- package/phone-preview-rcs-channel.js +319 -0
- package/phone-preview-rcs-chat.d.ts +31 -0
- package/phone-preview-rcs-chat.js +162 -0
- package/phone-preview.d.ts +30 -0
- package/phone-preview.js +120 -0
- package/utils/element.d.ts +7 -0
- package/utils/element.js +23 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import '@nectary/components/color-menu';
|
|
2
|
+
import '@nectary/components/color-menu-option';
|
|
3
|
+
import '@nectary/components/color-swatch';
|
|
4
|
+
import '@nectary/components/popover';
|
|
5
|
+
import '@nectary/components/select-button';
|
|
6
|
+
import { LitElement } from 'lit';
|
|
7
|
+
export declare class ColorSelect extends LitElement {
|
|
8
|
+
accessor open: boolean;
|
|
9
|
+
accessor value: string;
|
|
10
|
+
onClose(): void;
|
|
11
|
+
onOpen(): void;
|
|
12
|
+
onChange(e: CustomEvent<string>): void;
|
|
13
|
+
render(): import("lit").TemplateResult<1>;
|
|
14
|
+
}
|
|
15
|
+
declare const ScopedColorSelect_base: typeof LitElement;
|
|
16
|
+
export declare class ScopedColorSelect extends ScopedColorSelect_base {
|
|
17
|
+
static elementDefinitions: {
|
|
18
|
+
'sinch-color-select': typeof ColorSelect;
|
|
19
|
+
};
|
|
20
|
+
render(): import("lit").TemplateResult<1>;
|
|
21
|
+
}
|
|
22
|
+
declare global {
|
|
23
|
+
namespace JSX {
|
|
24
|
+
interface IntrinsicElements {
|
|
25
|
+
'sinch-labs-color-select': {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
interface HTMLElementTagNameMap {
|
|
29
|
+
'sinch-labs-color-select': {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export {};
|
package/color-select.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
|
8
|
+
import '@nectary/components/color-menu';
|
|
9
|
+
import '@nectary/components/color-menu-option';
|
|
10
|
+
import '@nectary/components/color-swatch';
|
|
11
|
+
import '@nectary/components/popover';
|
|
12
|
+
import '@nectary/components/select-button';
|
|
13
|
+
import { html, LitElement } from 'lit';
|
|
14
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
15
|
+
import { defineCustomElement } from './utils';
|
|
16
|
+
let ColorSelect = class ColorSelect extends LitElement {
|
|
17
|
+
accessor open = false;
|
|
18
|
+
accessor value = '';
|
|
19
|
+
onClose() {
|
|
20
|
+
this.open = false;
|
|
21
|
+
}
|
|
22
|
+
onOpen() {
|
|
23
|
+
this.open = true;
|
|
24
|
+
}
|
|
25
|
+
onChange(e) {
|
|
26
|
+
this.value = e.detail;
|
|
27
|
+
}
|
|
28
|
+
render() {
|
|
29
|
+
return html `
|
|
30
|
+
<sinch-popover
|
|
31
|
+
?open=${this.open}
|
|
32
|
+
aria-label="Select color"
|
|
33
|
+
orientation="bottom-right"
|
|
34
|
+
modal
|
|
35
|
+
@-close=${this.onClose}
|
|
36
|
+
>
|
|
37
|
+
<sinch-select-button
|
|
38
|
+
slot="target"
|
|
39
|
+
text=${this.value}
|
|
40
|
+
placeholder="Select color"
|
|
41
|
+
aria-label="Open color select"
|
|
42
|
+
@-click=${this.onOpen}
|
|
43
|
+
>
|
|
44
|
+
<sinch-color-swatch slot="icon" name="${this.value}" />
|
|
45
|
+
</sinch-select-button>
|
|
46
|
+
<sinch-color-menu
|
|
47
|
+
slot="content"
|
|
48
|
+
value=${this.value}
|
|
49
|
+
aria-label="Color menu"
|
|
50
|
+
@-change=${this.onChange}
|
|
51
|
+
>
|
|
52
|
+
<sinch-color-menu-option value="violet" />
|
|
53
|
+
</sinch-color-menu>
|
|
54
|
+
</sinch-popover>
|
|
55
|
+
</sinch-color-menu></sinch-popover>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
__decorate([
|
|
60
|
+
property({ type: 'boolean' })
|
|
61
|
+
], ColorSelect.prototype, "open", null);
|
|
62
|
+
__decorate([
|
|
63
|
+
property()
|
|
64
|
+
], ColorSelect.prototype, "value", null);
|
|
65
|
+
ColorSelect = __decorate([
|
|
66
|
+
customElement('sinch-color-select')
|
|
67
|
+
], ColorSelect);
|
|
68
|
+
export { ColorSelect };
|
|
69
|
+
export class ScopedColorSelect extends ScopedRegistryHost(LitElement) {
|
|
70
|
+
// Elements here will be registered against the tag names provided only
|
|
71
|
+
// in the shadow root for this element
|
|
72
|
+
static { this.elementDefinitions = {
|
|
73
|
+
'sinch-color-select': ColorSelect,
|
|
74
|
+
}; }
|
|
75
|
+
render() {
|
|
76
|
+
return html `<sinch-color-select></sinch-color-select>`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
defineCustomElement('sinch-labs-color-select', ScopedColorSelect);
|
package/imports.d.ts
ADDED
package/index.d.ts
ADDED
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import '@nectary/components/icon';
|
|
2
|
+
/**
|
|
3
|
+
* RCS channel preview component.
|
|
4
|
+
*
|
|
5
|
+
* @param props.color Brand color, used in the banner (if no image provided) and tabs.
|
|
6
|
+
* @param props.name Brand name.
|
|
7
|
+
* @param props.description Brand description.
|
|
8
|
+
* @param props.banner Brand banner image.
|
|
9
|
+
* @param props.logo Brand logo image.
|
|
10
|
+
* @param props.phone Brand phone numbers.
|
|
11
|
+
* @param props.website Brand website URLs.
|
|
12
|
+
* @param props.email Brand email addresses.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RcsChannelPreview: (props: {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
color: string;
|
|
18
|
+
banner: string;
|
|
19
|
+
logo: string;
|
|
20
|
+
phones: {
|
|
21
|
+
label: string;
|
|
22
|
+
number: string;
|
|
23
|
+
}[];
|
|
24
|
+
websites: {
|
|
25
|
+
label: string;
|
|
26
|
+
url: string;
|
|
27
|
+
}[];
|
|
28
|
+
emails: {
|
|
29
|
+
label: string;
|
|
30
|
+
address: string;
|
|
31
|
+
}[];
|
|
32
|
+
}) => Node | Node[];
|
|
33
|
+
type Props = Partial<Parameters<typeof RcsChannelPreview>[0]>;
|
|
34
|
+
type ElementProps = Partial<{
|
|
35
|
+
[K in keyof Props]: Props[K] | string;
|
|
36
|
+
}>;
|
|
37
|
+
declare global {
|
|
38
|
+
interface HTMLElementTagNameMap {
|
|
39
|
+
'sinch-labs-phone-preview-rcs-channel': ElementProps & HTMLElement;
|
|
40
|
+
}
|
|
41
|
+
namespace JSX {
|
|
42
|
+
interface IntrinsicElements {
|
|
43
|
+
'sinch-labs-phone-preview-rcs-channel': ElementProps & React.ClassAttributes<HTMLElement> & React.HTMLAttributes<HTMLElement>;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import '@nectary/components/icon';
|
|
2
|
+
import { customElement } from 'solid-element';
|
|
3
|
+
import { createSignal, For } from 'solid-js';
|
|
4
|
+
import html from 'solid-js/html';
|
|
5
|
+
import pkg from './package.json';
|
|
6
|
+
import { defineCustomElement } from './utils';
|
|
7
|
+
const style = `
|
|
8
|
+
:where(*, *::before, *::after) {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
padding: 0;
|
|
11
|
+
border: 0;
|
|
12
|
+
margin: 0;
|
|
13
|
+
font: inherit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.root {
|
|
17
|
+
--banner-color: var(--sinch-sys-color-surface-tertiary-active);
|
|
18
|
+
--logo-color: var(--sinch-sys-color-surface-secondary-default);
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-flow: column;
|
|
21
|
+
color: var(--sinch-sys-color-text-default);
|
|
22
|
+
|
|
23
|
+
& > img:first-of-type {
|
|
24
|
+
block-size: 70px;
|
|
25
|
+
margin-block-end: -40px;
|
|
26
|
+
background: var(--banner-color);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
& > img:last-of-type {
|
|
30
|
+
block-size: 64px;
|
|
31
|
+
inline-size: 64px;
|
|
32
|
+
border-radius: 100%;
|
|
33
|
+
background: var(--logo-color);
|
|
34
|
+
align-self: center;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
& > h1 {
|
|
38
|
+
padding: 8px 24px;
|
|
39
|
+
font: var(--sinch-sys-font-body-m);
|
|
40
|
+
text-align: center;
|
|
41
|
+
text-wrap: balance;
|
|
42
|
+
word-wrap: break-word;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
& > p {
|
|
46
|
+
padding-inline: 24px;
|
|
47
|
+
font: var(--sinch-sys-font-body-xs);
|
|
48
|
+
text-align: center;
|
|
49
|
+
text-wrap: balance;
|
|
50
|
+
word-wrap: break-word;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
& > .actions {
|
|
54
|
+
align-self: center;
|
|
55
|
+
padding-block: 32px 24px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
& > .tabs {
|
|
59
|
+
padding-block-end: 8px;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.actions {
|
|
64
|
+
display: grid;
|
|
65
|
+
grid-auto-columns: 1fr;
|
|
66
|
+
grid-auto-flow: column;
|
|
67
|
+
gap: 24px;
|
|
68
|
+
font: var(--sinch-sys-font-body-xs);
|
|
69
|
+
|
|
70
|
+
& > a {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-flow: column;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 2px;
|
|
75
|
+
color: inherit;
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
|
|
78
|
+
&[inert] {
|
|
79
|
+
--sinch-global-color-icon: currentColor;
|
|
80
|
+
color: var(--sinch-sys-color-text-muted);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
.info {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-flow: column;
|
|
89
|
+
font: var(--sinch-sys-font-body-xs);
|
|
90
|
+
|
|
91
|
+
& > a {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template:
|
|
94
|
+
"icon contact" auto
|
|
95
|
+
"icon label " auto
|
|
96
|
+
/ auto 1fr;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: 0 16px;
|
|
99
|
+
padding: 8px 16px;
|
|
100
|
+
border-block-end: 1px solid
|
|
101
|
+
var(--sinch-sys-color-surface-secondary-active);
|
|
102
|
+
color: currentColor;
|
|
103
|
+
word-break: break-all;
|
|
104
|
+
text-decoration: none;
|
|
105
|
+
|
|
106
|
+
& > sinch-icon {
|
|
107
|
+
grid-area: icon;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
& > span {
|
|
111
|
+
grid-area: contact;
|
|
112
|
+
|
|
113
|
+
&::before {
|
|
114
|
+
content: "\\200b";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
& > p {
|
|
119
|
+
grid-area: label;
|
|
120
|
+
|
|
121
|
+
&::before {
|
|
122
|
+
content: "\\200b";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&[inert] {
|
|
127
|
+
--sinch-global-color-icon: currentColor;
|
|
128
|
+
color: var(--sinch-sys-color-text-muted);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.tabs {
|
|
134
|
+
--highlight-color: var(--sinch-sys-color-text-default);
|
|
135
|
+
display: flex;
|
|
136
|
+
|
|
137
|
+
& > button {
|
|
138
|
+
flex: 1;
|
|
139
|
+
padding-block-end: 10px;
|
|
140
|
+
border-block-end: 2px solid transparent;
|
|
141
|
+
outline: none;
|
|
142
|
+
background: transparent;
|
|
143
|
+
color: var(--sinch-sys-color-text-disabled);
|
|
144
|
+
font: var(--sinch-sys-font-desktop-title-xs);
|
|
145
|
+
|
|
146
|
+
&.active {
|
|
147
|
+
color: var(--sinch-sys-color-primary-default);
|
|
148
|
+
border-block-end: 2px solid var(--highlight-color);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
.options {
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-flow: column;
|
|
157
|
+
font: var(--sinch-sys-font-body-xs);
|
|
158
|
+
|
|
159
|
+
& > header {
|
|
160
|
+
padding-block-end: 8px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
& > span {
|
|
164
|
+
font: var(--sinch-sys-font-body-xxs);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
& > button {
|
|
168
|
+
padding: 4px;
|
|
169
|
+
outline: none;
|
|
170
|
+
background: transparent;
|
|
171
|
+
text-align: start;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
& > hr {
|
|
175
|
+
border-color: var(--sinch-sys-color-surface-secondary-active);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
const Actions = (props) => {
|
|
180
|
+
const number = () => props.phones.at(0)?.number ?? '';
|
|
181
|
+
const url = () => props.websites.at(0)?.url ?? '';
|
|
182
|
+
const email = () => props.emails.at(0)?.address ?? '';
|
|
183
|
+
const numberHref = () => `tel:${number()}`;
|
|
184
|
+
const urlHref = url;
|
|
185
|
+
const emailHref = () => `mailto:${email()}`;
|
|
186
|
+
return html `
|
|
187
|
+
<section class="actions">
|
|
188
|
+
<a inert=${() => number() === ''} target="_blank" href=${numberHref}>
|
|
189
|
+
<sinch-icon name="call" />
|
|
190
|
+
Call
|
|
191
|
+
</a>
|
|
192
|
+
<a inert=${() => url() === ''} target="_blank" href=${urlHref}>
|
|
193
|
+
<sinch-icon name="public" />
|
|
194
|
+
Website
|
|
195
|
+
</a>
|
|
196
|
+
<a inert=${() => email() === ''} target="_blank" href=${emailHref}>
|
|
197
|
+
<sinch-icon name="mail" />
|
|
198
|
+
Email
|
|
199
|
+
</a>
|
|
200
|
+
</section>
|
|
201
|
+
`;
|
|
202
|
+
};
|
|
203
|
+
const Info = (props) => {
|
|
204
|
+
const phones = () => ((props.phones.length > 0)
|
|
205
|
+
? props.phones
|
|
206
|
+
: [{ label: 'Contact us', number: '+1234567890' }]);
|
|
207
|
+
const websites = () => ((props.websites.length > 0)
|
|
208
|
+
? props.websites
|
|
209
|
+
: [{ label: 'Contact us', url: 'https://company.com' }]);
|
|
210
|
+
const emails = () => ((props.emails.length > 0)
|
|
211
|
+
? props.emails
|
|
212
|
+
: [{ label: 'Contact us', address: 'mail@company.com' }]);
|
|
213
|
+
return html `
|
|
214
|
+
<section class="info">
|
|
215
|
+
<${For} each=${phones}>
|
|
216
|
+
${({ label, number }) => html `
|
|
217
|
+
<a
|
|
218
|
+
inert=${() => props.phones.length === 0}
|
|
219
|
+
target="_blank"
|
|
220
|
+
href=${`tel:${number}`}
|
|
221
|
+
>
|
|
222
|
+
<sinch-icon name="call" />
|
|
223
|
+
<span>${number}</span>
|
|
224
|
+
<p>${label}</p>
|
|
225
|
+
</a>
|
|
226
|
+
`}
|
|
227
|
+
<//>
|
|
228
|
+
<${For} each=${websites}>
|
|
229
|
+
${({ label, url }) => html `
|
|
230
|
+
<a inert=${() => props.websites.length === 0} target="_blank" href=${url}>
|
|
231
|
+
<sinch-icon name="public" />
|
|
232
|
+
<span>${url}</span>
|
|
233
|
+
<p>${label}</p>
|
|
234
|
+
</a>
|
|
235
|
+
`}
|
|
236
|
+
<//>
|
|
237
|
+
<${For} each=${emails}>
|
|
238
|
+
${({ label, address }) => html `
|
|
239
|
+
<a
|
|
240
|
+
inert=${() => props.emails.length === 0}
|
|
241
|
+
target="_blank"
|
|
242
|
+
href=${`mailto:${address}`}
|
|
243
|
+
>
|
|
244
|
+
<sinch-icon name="mail" />
|
|
245
|
+
<span>${address}</span>
|
|
246
|
+
<p>${label}</p>
|
|
247
|
+
</a>
|
|
248
|
+
`}
|
|
249
|
+
<//>
|
|
250
|
+
</section>
|
|
251
|
+
`;
|
|
252
|
+
};
|
|
253
|
+
const Tabs = (props) => html `
|
|
254
|
+
<section class="tabs" style=${() => ({ '--highlight-color': props.color })}>
|
|
255
|
+
<${For} each=${['Info', 'Options']}>
|
|
256
|
+
${(label, i) => html `
|
|
257
|
+
<button
|
|
258
|
+
class=${() => (i() === props.tab ? 'active' : '')}
|
|
259
|
+
on:click=${() => props.onTab?.(i())}
|
|
260
|
+
>
|
|
261
|
+
${label}
|
|
262
|
+
</button>
|
|
263
|
+
`}
|
|
264
|
+
<//>
|
|
265
|
+
</section>
|
|
266
|
+
`;
|
|
267
|
+
const Options = () => html `
|
|
268
|
+
<section class="options">
|
|
269
|
+
<header>Notifications</header>
|
|
270
|
+
<span>Business</span>
|
|
271
|
+
<button>Block & report spam</button>
|
|
272
|
+
<hr />
|
|
273
|
+
<button>View Privacy Policy</button>
|
|
274
|
+
<hr />
|
|
275
|
+
<button>View Terms of Service</button>
|
|
276
|
+
<hr />
|
|
277
|
+
<button>Learn mode</button>
|
|
278
|
+
</section>
|
|
279
|
+
`;
|
|
280
|
+
/**
|
|
281
|
+
* RCS channel preview component.
|
|
282
|
+
*
|
|
283
|
+
* @param props.color Brand color, used in the banner (if no image provided) and tabs.
|
|
284
|
+
* @param props.name Brand name.
|
|
285
|
+
* @param props.description Brand description.
|
|
286
|
+
* @param props.banner Brand banner image.
|
|
287
|
+
* @param props.logo Brand logo image.
|
|
288
|
+
* @param props.phone Brand phone numbers.
|
|
289
|
+
* @param props.website Brand website URLs.
|
|
290
|
+
* @param props.email Brand email addresses.
|
|
291
|
+
*/
|
|
292
|
+
export const RcsChannelPreview = (props) => {
|
|
293
|
+
const [tab, setTab] = createSignal(0);
|
|
294
|
+
const transparentIcon = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
|
|
295
|
+
return html `
|
|
296
|
+
<style>
|
|
297
|
+
${style}
|
|
298
|
+
</style>
|
|
299
|
+
<section class="root" style=${() => ({ '--banner-color': props.color })}>
|
|
300
|
+
<img src=${() => (props.banner !== '' ? props.banner : transparentIcon)} alt="" />
|
|
301
|
+
<img src=${() => (props.logo !== '' ? props.logo : transparentIcon)} alt="" />
|
|
302
|
+
<h1>${() => (props.name !== '' ? props.name : 'Brand name')}</h1>
|
|
303
|
+
<p>${() => (props.description !== '' ? props.description : 'Brand description')}</p>
|
|
304
|
+
<${Actions} ...${props} />
|
|
305
|
+
<${Tabs} color=${() => props.color} tab=${tab} onTab=${setTab} />
|
|
306
|
+
${() => (tab() === 0 ? html `<${Info} ...${props} />` : html `<${Options} />`)}
|
|
307
|
+
</section>
|
|
308
|
+
`;
|
|
309
|
+
};
|
|
310
|
+
defineCustomElement('sinch-labs-phone-preview-rcs-channel', customElement(`sinch-labs-phone-preview-rcs-channel-${pkg.version}`, {
|
|
311
|
+
name: '',
|
|
312
|
+
description: '',
|
|
313
|
+
color: '',
|
|
314
|
+
banner: '',
|
|
315
|
+
logo: '',
|
|
316
|
+
phones: [],
|
|
317
|
+
websites: [],
|
|
318
|
+
emails: [],
|
|
319
|
+
}, RcsChannelPreview));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import '@nectary/components/icon';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* RCS chat preview component.
|
|
5
|
+
*
|
|
6
|
+
* @param props.name Brand name.
|
|
7
|
+
* @param props.description Brand description.
|
|
8
|
+
* @param props.logo Brand logo image.
|
|
9
|
+
* @param props.messages List of messages.
|
|
10
|
+
*/
|
|
11
|
+
export declare const RcsChatPreview: (props: {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
logo: string;
|
|
15
|
+
messages: string[];
|
|
16
|
+
}) => Node | Node[];
|
|
17
|
+
type Props = Parameters<typeof RcsChatPreview>[0];
|
|
18
|
+
type ElementProps = Partial<{
|
|
19
|
+
[K in keyof Props]: Props[K] | string;
|
|
20
|
+
}>;
|
|
21
|
+
declare global {
|
|
22
|
+
interface HTMLElementTagNameMap {
|
|
23
|
+
'sinch-labs-phone-preview-rcs-chat': ElementProps & HTMLElement;
|
|
24
|
+
}
|
|
25
|
+
namespace JSX {
|
|
26
|
+
interface IntrinsicElements {
|
|
27
|
+
'sinch-labs-phone-preview-rcs-chat': ElementProps & React.ClassAttributes<HTMLElement> & React.HTMLAttributes<HTMLElement>;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import '@nectary/components/icon';
|
|
2
|
+
import { customElement } from 'solid-element';
|
|
3
|
+
import { For } from 'solid-js';
|
|
4
|
+
import html from 'solid-js/html';
|
|
5
|
+
import pkg from './package.json';
|
|
6
|
+
import { defineCustomElement } from './utils';
|
|
7
|
+
const style = `
|
|
8
|
+
:where(*, *::before, *::after) {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
padding: 0;
|
|
11
|
+
border: 0;
|
|
12
|
+
margin: 0;
|
|
13
|
+
font: inherit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.root {
|
|
17
|
+
--logo-color: var(--sinch-sys-color-surface-secondary-default);
|
|
18
|
+
block-size: 100%;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-flow: column;
|
|
21
|
+
|
|
22
|
+
& > header {
|
|
23
|
+
display: flex;
|
|
24
|
+
gap: 8px;
|
|
25
|
+
padding: 12px 4px;
|
|
26
|
+
background: var(--sinch-sys-color-surface-tertiary-default);
|
|
27
|
+
font: var(--sinch-sys-font-body-m);
|
|
28
|
+
|
|
29
|
+
& > img {
|
|
30
|
+
block-size: 24px;
|
|
31
|
+
inline-size: 24px;
|
|
32
|
+
margin-inline-start: 8px;
|
|
33
|
+
border-radius: 100%;
|
|
34
|
+
background: var(--logo-color);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
& > h1 {
|
|
38
|
+
flex: 1;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
min-inline-size: 0;
|
|
41
|
+
text-wrap: nowrap;
|
|
42
|
+
text-overflow: ellipsis;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
& > div {
|
|
47
|
+
flex: 1;
|
|
48
|
+
overflow-y: auto;
|
|
49
|
+
scrollbar-width: none;
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-flow: column;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
padding: 8px;
|
|
54
|
+
margin-block-end: 8px;
|
|
55
|
+
|
|
56
|
+
& > img {
|
|
57
|
+
block-size: 64px;
|
|
58
|
+
inline-size: 64px;
|
|
59
|
+
border-radius: 100%;
|
|
60
|
+
align-self: center;
|
|
61
|
+
background: var(--logo-color);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
& > p {
|
|
65
|
+
padding-inline: 24px;
|
|
66
|
+
font: var(--sinch-sys-font-body-xs);
|
|
67
|
+
text-align: center;
|
|
68
|
+
text-wrap: balance;
|
|
69
|
+
word-wrap: break-word;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
& > hr {
|
|
73
|
+
border-block-end: 1px solid var(--sinch-sys-color-border-subtle);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
& > footer {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 8px;
|
|
81
|
+
font: var(--sinch-sys-font-body-xs);
|
|
82
|
+
|
|
83
|
+
& > div {
|
|
84
|
+
flex: 1;
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 8px;
|
|
88
|
+
padding: 8px 16px;
|
|
89
|
+
border-radius: 24px;
|
|
90
|
+
background: var(--sinch-sys-color-surface-primary-active);
|
|
91
|
+
|
|
92
|
+
& > span {
|
|
93
|
+
flex: 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.message {
|
|
100
|
+
padding: 8px 12px;
|
|
101
|
+
margin-inline-end: 8px;
|
|
102
|
+
border-radius: 16px;
|
|
103
|
+
border-end-start-radius: 0;
|
|
104
|
+
background: var(--sinch-sys-color-feedback-info-subtle);
|
|
105
|
+
font: var(--sinch-sys-font-body-xs);
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
const Message = (props) => html `<section class="message">${props.message}</section>`;
|
|
109
|
+
/**
|
|
110
|
+
* RCS chat preview component.
|
|
111
|
+
*
|
|
112
|
+
* @param props.name Brand name.
|
|
113
|
+
* @param props.description Brand description.
|
|
114
|
+
* @param props.logo Brand logo image.
|
|
115
|
+
* @param props.messages List of messages.
|
|
116
|
+
*/
|
|
117
|
+
export const RcsChatPreview = (props) => {
|
|
118
|
+
const transparentIcon = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
|
|
119
|
+
return html ` <style>
|
|
120
|
+
${style}
|
|
121
|
+
</style>
|
|
122
|
+
<section class="root">
|
|
123
|
+
<header>
|
|
124
|
+
<sinch-icon name="arrow_back" />
|
|
125
|
+
<img
|
|
126
|
+
src=${() => (props.logo !== '' ? props.logo : transparentIcon)}
|
|
127
|
+
alt=""
|
|
128
|
+
/>
|
|
129
|
+
<h1>${() => (props.name !== '' ? props.name : 'Brand name')}</h1>
|
|
130
|
+
<sinch-icon name="verified_user" />
|
|
131
|
+
<sinch-icon name="more_vert" />
|
|
132
|
+
</header>
|
|
133
|
+
<div>
|
|
134
|
+
<img
|
|
135
|
+
src=${() => (props.logo !== '' ? props.logo : transparentIcon)}
|
|
136
|
+
alt=""
|
|
137
|
+
/>
|
|
138
|
+
<p>
|
|
139
|
+
${() => (props.description !== '' ? props.description : 'Brand description')}
|
|
140
|
+
</p>
|
|
141
|
+
<hr />
|
|
142
|
+
<${For} each=${() => props.messages}>
|
|
143
|
+
${(message) => html `<${Message} message=${message} />`}
|
|
144
|
+
<//>
|
|
145
|
+
</div>
|
|
146
|
+
<footer>
|
|
147
|
+
<sinch-icon name="add_circle" />
|
|
148
|
+
<sinch-icon name="photo_camera" />
|
|
149
|
+
<div>
|
|
150
|
+
<span>RCS Message</span>
|
|
151
|
+
<sinch-icon name="sentiment_satisfied" />
|
|
152
|
+
<sinch-icon name="mic" />
|
|
153
|
+
</div>
|
|
154
|
+
</footer>
|
|
155
|
+
</section>`;
|
|
156
|
+
};
|
|
157
|
+
defineCustomElement('sinch-labs-phone-preview-rcs-chat', customElement(`sinch-labs-phone-preview-rcs-chat-${pkg.version}`, {
|
|
158
|
+
name: '',
|
|
159
|
+
description: '',
|
|
160
|
+
logo: '',
|
|
161
|
+
messages: [],
|
|
162
|
+
}, RcsChatPreview));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container for channel previews in a styled phone container.
|
|
3
|
+
* This container uses a custom scaling where the internal elements are scaled to fit the container from a fixed size.
|
|
4
|
+
* Because of the fixed size, absolute units (px) are preferred over relative units (rem, em) for the internal elements.
|
|
5
|
+
*
|
|
6
|
+
* @param props.locale Clock locale.
|
|
7
|
+
* @param props.clock Clock `Intl.DateTimeFormat` options.
|
|
8
|
+
* @param props.children Content to display in the phone container.
|
|
9
|
+
*/
|
|
10
|
+
declare const PhonePreview: (props: {
|
|
11
|
+
locale: string;
|
|
12
|
+
clock: Intl.DateTimeFormatOptions;
|
|
13
|
+
}, options: {
|
|
14
|
+
element: any;
|
|
15
|
+
}) => Node | Node[];
|
|
16
|
+
type Props = Partial<Parameters<typeof PhonePreview>[0]>;
|
|
17
|
+
type ElementProps = Partial<{
|
|
18
|
+
[K in keyof Props]: Props[K] | string;
|
|
19
|
+
}>;
|
|
20
|
+
declare global {
|
|
21
|
+
interface HTMLElementTagNameMap {
|
|
22
|
+
'sinch-labs-phone-preview': ElementProps & HTMLElement;
|
|
23
|
+
}
|
|
24
|
+
namespace JSX {
|
|
25
|
+
interface IntrinsicElements {
|
|
26
|
+
'sinch-labs-phone-preview': ElementProps & React.ClassAttributes<HTMLElement> & React.HTMLAttributes<HTMLElement>;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export {};
|
package/phone-preview.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { customElement } from 'solid-element';
|
|
2
|
+
import { createComputed, createMemo, createSignal, onCleanup, onMount, } from 'solid-js';
|
|
3
|
+
import html from 'solid-js/html';
|
|
4
|
+
import pkg from './package.json';
|
|
5
|
+
import { defineCustomElement } from './utils';
|
|
6
|
+
const style = `
|
|
7
|
+
:where(*, *::before, *::after) {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
padding: 0;
|
|
10
|
+
border: 0;
|
|
11
|
+
margin: 0;
|
|
12
|
+
font: inherit;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:host {
|
|
16
|
+
--base-size: 288px; /* 18rem */
|
|
17
|
+
--aspect-ratio: 1 / 2.1;
|
|
18
|
+
--scale: 1;
|
|
19
|
+
inline-size: min(100%, var(--base-size));
|
|
20
|
+
aspect-ratio: var(--aspect-ratio);
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
display: block;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
section {
|
|
26
|
+
position: relative;
|
|
27
|
+
inline-size: var(--base-size);
|
|
28
|
+
aspect-ratio: var(--aspect-ratio);
|
|
29
|
+
scale: var(--scale);
|
|
30
|
+
transform-origin: top left;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-flow: column;
|
|
34
|
+
padding: 12px;
|
|
35
|
+
border: 1px solid var(--sinch-sys-color-border-strong);
|
|
36
|
+
border-radius: 32px;
|
|
37
|
+
background: var(--sinch-sys-color-surface-primary-default);
|
|
38
|
+
|
|
39
|
+
& > header {
|
|
40
|
+
position: sticky;
|
|
41
|
+
inset-block-start: 0;
|
|
42
|
+
display: flex;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
padding: 16px;
|
|
45
|
+
background: var(--sinch-sys-color-surface-primary-default);
|
|
46
|
+
font: var(--sinch-sys-font-body-xxs);
|
|
47
|
+
|
|
48
|
+
& > svg {
|
|
49
|
+
inline-size: 48px;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
& > div {
|
|
54
|
+
flex: 1;
|
|
55
|
+
overflow-y: auto;
|
|
56
|
+
scrollbar-width: none;
|
|
57
|
+
border-end-start-radius: 16px;
|
|
58
|
+
border-end-end-radius: 16px;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
const StatusSvg = () => html `
|
|
63
|
+
<svg viewBox="0 0 50 12">
|
|
64
|
+
<path
|
|
65
|
+
d="M13.2 2.4h-.7c-.4 0-.7.3-.7.7v6.2c0 .4.3.8.7.8h.7c.4 0 .7-.4.7-.8V3.1c0-.4-.3-.7-.7-.7ZM9.4 4.2h.6c.4 0 .7.3.7.7v4.5c0 .3-.3.7-.7.7h-.6c-.4 0-.7-.4-.7-.7V4.9c0-.4.3-.7.7-.7ZM6.9 6h-.7c-.4 0-.7.3-.7.7v2.7c0 .4.3.7.7.7h.7c.4 0 .7-.3.7-.7V6.7c0-.4-.3-.7-.7-.7ZM3.7 7.3H3c-.3 0-.6.4-.6.7v1.4c0 .4.3.7.6.7h.7c.4 0 .7-.3.7-.7V8c0-.3-.3-.7-.7-.7Zm19.4-3.7c1.7 0 3.3.7 4.5 1.8h.3l.8-.9.1-.1-.1-.2a8 8 0 0 0-11.1 0l-.1.2.1.1.8.9h.3a6.7 6.7 0 0 1 4.4-1.8Zm0 2.8a4 4 0 0 1 2.5 1h.3l.9-.9v-.3a5.4 5.4 0 0 0-7.3 0v.3l.9.9h.3c.7-.6 1.5-1 2.4-1Zm1.8 1.9-.1.2-1.5 1.5-.2.1-.1-.1-1.5-1.5-.1-.2.1-.1c1-.8 2.3-.8 3.3 0l.1.1Z"
|
|
66
|
+
/>
|
|
67
|
+
<rect width="12.6" height="5.4" x="33.3" y="3.5" rx=".9" />
|
|
68
|
+
<path fill="#999" d="M48 4.9v2.7c.5-.3.9-.8.9-1.4 0-.6-.4-1.1-.9-1.3Z" />
|
|
69
|
+
<path
|
|
70
|
+
fill="none"
|
|
71
|
+
stroke="#999"
|
|
72
|
+
stroke-width=".7"
|
|
73
|
+
d="M32.3 4.4a2 2 0 0 1 1.9-1.9H45c1.1 0 2 .9 2 1.9V8c0 1.1-.9 1.9-2 1.9H34.2c-1 0-1.9-.8-1.9-1.9V4.4Z"
|
|
74
|
+
/>
|
|
75
|
+
</svg>
|
|
76
|
+
`;
|
|
77
|
+
/**
|
|
78
|
+
* Container for channel previews in a styled phone container.
|
|
79
|
+
* This container uses a custom scaling where the internal elements are scaled to fit the container from a fixed size.
|
|
80
|
+
* Because of the fixed size, absolute units (px) are preferred over relative units (rem, em) for the internal elements.
|
|
81
|
+
*
|
|
82
|
+
* @param props.locale Clock locale.
|
|
83
|
+
* @param props.clock Clock `Intl.DateTimeFormat` options.
|
|
84
|
+
* @param props.children Content to display in the phone container.
|
|
85
|
+
*/
|
|
86
|
+
const PhonePreview = (props, options) => {
|
|
87
|
+
const host = options.element;
|
|
88
|
+
const observer = new ResizeObserver(() => {
|
|
89
|
+
const style = getComputedStyle(host);
|
|
90
|
+
const baseSize = parseFloat(style.getPropertyValue('--base-size'));
|
|
91
|
+
const currentSize = host.getBoundingClientRect().width;
|
|
92
|
+
host.style.setProperty('--scale', `${currentSize / baseSize}`);
|
|
93
|
+
});
|
|
94
|
+
onMount(() => {
|
|
95
|
+
const section = host.shadowRoot.querySelector('section');
|
|
96
|
+
observer.observe(host);
|
|
97
|
+
observer.observe(section);
|
|
98
|
+
});
|
|
99
|
+
onCleanup(() => observer.disconnect());
|
|
100
|
+
const fmt = createMemo(() => Intl.DateTimeFormat(props.locale, props.clock));
|
|
101
|
+
const [clock, setClock] = createSignal();
|
|
102
|
+
const interval = setInterval(() => setClock(fmt().format()), 60000);
|
|
103
|
+
createComputed(() => setClock(fmt().format()));
|
|
104
|
+
onCleanup(() => clearInterval(interval));
|
|
105
|
+
return html `
|
|
106
|
+
<style>
|
|
107
|
+
${style}
|
|
108
|
+
</style>
|
|
109
|
+
<section>
|
|
110
|
+
<header>
|
|
111
|
+
<span>${clock}</span>
|
|
112
|
+
<${StatusSvg} />
|
|
113
|
+
</header>
|
|
114
|
+
<div>
|
|
115
|
+
<slot />
|
|
116
|
+
</div>
|
|
117
|
+
</section>
|
|
118
|
+
`;
|
|
119
|
+
};
|
|
120
|
+
defineCustomElement('sinch-labs-phone-preview', customElement(`sinch-labs-phone-preview-${pkg.version}`, { locale: 'en-US', clock: { hour: '2-digit', minute: '2-digit' } }, PhonePreview));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const defineCustomElement: (name: string, constructor: CustomElementConstructor) => void;
|
|
2
|
+
export declare const setLabRegistry: (registry: CustomElementRegistry) => void;
|
|
3
|
+
declare global {
|
|
4
|
+
interface ShadowRootInit {
|
|
5
|
+
customElements?: CustomElementRegistry;
|
|
6
|
+
}
|
|
7
|
+
}
|
package/utils/element.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const nectaryDefinitions = new Map();
|
|
2
|
+
let nectaryRegistry = null;
|
|
3
|
+
export const defineCustomElement = (name, constructor) => {
|
|
4
|
+
if (nectaryRegistry !== null) {
|
|
5
|
+
if (nectaryRegistry.get(name) == null) {
|
|
6
|
+
nectaryRegistry.define(name, constructor);
|
|
7
|
+
}
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
nectaryDefinitions.set(name, constructor);
|
|
11
|
+
};
|
|
12
|
+
export const setLabRegistry = (registry) => {
|
|
13
|
+
if (nectaryRegistry !== null) {
|
|
14
|
+
throw new Error('Nectary registry already set');
|
|
15
|
+
}
|
|
16
|
+
nectaryRegistry = registry;
|
|
17
|
+
for (const [name, ctor] of nectaryDefinitions.entries()) {
|
|
18
|
+
if (nectaryRegistry.get(name) == null) {
|
|
19
|
+
nectaryRegistry.define(name, ctor);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
nectaryDefinitions.clear();
|
|
23
|
+
};
|
package/utils/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineCustomElement, setLabRegistry } from './element';
|
package/utils/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineCustomElement, setLabRegistry } from './element';
|