Skip to content

Commit

Permalink
feat(chat-bubble): add ChatBubble component (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
mimshins authored Oct 16, 2024
1 parent d3bc23d commit e9b6b06
Show file tree
Hide file tree
Showing 13 changed files with 555 additions and 6 deletions.
4 changes: 2 additions & 2 deletions docs/dev/components/[component].md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import '../../../src/banner';
import '../../../src/base-button';
import '../../../src/bottom-navigation';
import '../../../src/bottom-navigation-item';
import '../../../src/bottom-sheet';
import '../../../src/base-button';
import '../../../src/button';
import '../../../src/base-button';
import '../../../src/bottom-sheet';
import '../../../src/chat-bubble';
import '../../../src/checkbox';
import '../../../src/divider';
import '../../../src/icon-button';
Expand Down
16 changes: 14 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,25 @@
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<link
rel="stylesheet"
href="./styles/font.css"
/>
<link
rel="stylesheet"
href="./styles/theme.css"
/>
</head>
<body>
<body dir="rtl">
<div id="root"></div>
<script type="module">
import { html, render } from "lit";

render(html` <div></div> `, document.querySelector("#root"));
const root = document.getElementById("root");

if (!root) throw new Error("Expects a root element.");

render(html``, root);
</script>
</body>
</html>
76 changes: 76 additions & 0 deletions src/chat-bubble/chat-bubble-base.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { css } from "lit";

const styles = css`
*,
*::before,
*::after {
box-sizing: border-box;
}
.root {
display: flex;
flex-direction: column;
gap: var(--tap-sys-spacing-2);
padding: var(--tap-sys-spacing-3) var(--tap-sys-spacing-6);
border-radius: var(--chat-bubble-base-radius);
background-color: var(--chat-bubble-base-bg-color);
min-width: 6rem;
max-width: 17rem;
}
.root.fully-rounded {
--chat-bubble-base-radius: var(--tap-sys-radius-5);
}
.root.in {
--chat-bubble-base-bg-color: var(--tap-sys-color-surface-tertiary);
--chat-bubble-base-color: var(--tap-sys-color-content-primary);
--chat-bubble-base-footer-color: var(--tap-sys-color-content-tertiary);
--chat-bubble-base-footer-flex-direction: row;
}
.root.out {
--chat-bubble-base-bg-color: var(--tap-sys-color-surface-accent);
--chat-bubble-base-color: var(--tap-sys-color-content-on-accent);
--chat-bubble-base-footer-color: var(--chat-bubble-base-color);
--chat-bubble-base-footer-flex-direction: row-reverse;
}
.root:not(.fully-rounded).in {
--chat-bubble-base-radius: var(--tap-sys-radius-5) var(--tap-sys-radius-1)
var(--tap-sys-radius-5) var(--tap-sys-radius-5);
}
.root:not(.fully-rounded).out {
--chat-bubble-base-radius: var(--tap-sys-radius-1) var(--tap-sys-radius-5)
var(--tap-sys-radius-5) var(--tap-sys-radius-5);
}
.body {
font-family: var(--tap-sys-typography-body-sm-font);
font-size: var(--tap-sys-typography-body-sm-size);
line-height: var(--tap-sys-typography-body-sm-height);
font-weight: var(--tap-sys-typography-body-sm-weight);
color: var(--chat-bubble-base-color);
}
.footer {
font-family: var(--tap-sys-typography-body-xs-font);
font-size: var(--tap-sys-typography-body-xs-size);
line-height: var(--tap-sys-typography-body-xs-height);
font-weight: var(--tap-sys-typography-body-xs-weight);
color: var(--chat-bubble-base-footer-color);
display: flex;
flex-direction: var(--chat-bubble-base-footer-flex-direction);
gap: var(--tap-sys-spacing-3);
}
`;

export default styles;
79 changes: 79 additions & 0 deletions src/chat-bubble/chat-bubble-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { logger } from "../utils";
import styles from "./chat-bubble-base.style";
import { AUTHORS, BaseSlots } from "./constants";

@customElement("tap-chat-bubble-base")
export class ChatBubbleBase extends LitElement {
public static override readonly styles = [styles];

@property({ type: String })
public author!: (typeof AUTHORS)[number];

@property({ type: String })
public timestamp!: string;

@property({ type: Boolean, attribute: "fully-rounded" })
public fullyRounded: boolean = false;

constructor() {
super();
}

private _renderFooter() {
if (!this.timestamp) {
logger(
`Expected valid \`timestamp\` prop. received: \`${this.timestamp}\`.`,
"ChatBubble",
"error",
);

return nothing;
}

return html`
<div
class="footer"
part="footer"
>
<slot name=${BaseSlots.FOOTER}></slot>
<span>${this.timestamp}</span>
</div>
`;
}

protected override render() {
if (!AUTHORS.includes(this.author)) {
logger(
`Expected valid \`author\` prop. received: \`${this.author}\`.`,
"ChatBubble",
"error",
);

return nothing;
}

const rootClasses = classMap({
"fully-rounded": this.fullyRounded,
in: this.author === "in",
out: this.author === "out",
});

return html`
<div
class="root ${rootClasses}"
part="root"
>
<div
class="body"
part="body"
>
<slot name=${BaseSlots.BODY}></slot>
</div>
${this._renderFooter()}
</div>
`;
}
}
53 changes: 53 additions & 0 deletions src/chat-bubble/chat-bubble-in.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { css } from "lit";

const styles = css`
*,
*::before,
*::after {
box-sizing: border-box;
}
.root {
--chat-bubble-in-icon-color: currentColor;
display: flex;
}
.root.seen {
--chat-bubble-in-icon-color: var(--tap-sys-color-content-accent);
}
.root:not(.failed) .base {
margin-right: var(--tap-sys-spacing-4);
}
.failure-indicator {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-right: var(--tap-sys-spacing-4);
margin-left: var(--tap-sys-spacing-4);
fill: var(--tap-sys-color-content-negative);
}
.status {
display: flex;
align-items: center;
gap: var(--tap-sys-spacing-3);
}
.status > svg {
width: 18px;
height: 18px;
fill: var(--chat-bubble-in-icon-color);
}
`;

export default styles;
93 changes: 93 additions & 0 deletions src/chat-bubble/chat-bubble-in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import "./chat-bubble-base";

import { html, LitElement, nothing } from "lit";
import { property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import {
BaseSlots,
STATUS_TO_ICON_MAP,
STATUS_TO_LOCALE_MAP,
type STATES,
} from "./constants";

export class ChatBubbleIn extends LitElement {
/**
* The timestamp of chat element.
*/
@property({ type: String })
public timestamp!: string;

/**
* The status of the chat element.
*
* @default "sent"
*/
@property({ type: String })
public status: (typeof STATES)[number] = "sent";

/**
* Whether or not the bubble should be fully rounded.
*
* @default false
*/
@property({ type: Boolean, attribute: "fully-rounded" })
public fullyRounded: boolean = false;

private _renderFailureIndicator() {
if (this.status !== "failed") return nothing;

const icon = STATUS_TO_ICON_MAP.failed;

return html`
<div
class="failure-indicator"
part="failure-indicator"
>
${icon}
</div>
`;
}

private _renderStatus() {
if (this.status === "failed") return nothing;

const stateMessage = STATUS_TO_LOCALE_MAP[this.status];
const icon = STATUS_TO_ICON_MAP[this.status];

return html`
<div
slot=${BaseSlots.FOOTER}
class="status"
part="status"
>
${icon}
<span>${stateMessage}</span>
</div>
`;
}

protected override render() {
const rootClasses = classMap({
[String(this.status)]: Boolean(this.status),
});

return html`
<div
class="root ${rootClasses}"
part="root"
>
${this._renderFailureIndicator()}
<tap-chat-bubble-base
class="base"
part="base"
author="in"
?fully-rounded=${this.fullyRounded}
timestamp=${this.timestamp}
>
<slot slot=${BaseSlots.BODY}></slot>
${this._renderStatus()}
</tap-chat-bubble-base>
</div>
`;
}
}
31 changes: 31 additions & 0 deletions src/chat-bubble/chat-bubble-out.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { css } from "lit";

const styles = css`
*,
*::before,
*::after {
box-sizing: border-box;
}
.root {
--chat-bubble-out-leading-space: var(--tap-sys-spacing-11);
display: flex;
flex-direction: row-reverse;
}
.root.has-avatar {
--chat-bubble-out-leading-space: 0;
}
.root .base {
margin-left: var(--chat-bubble-out-leading-space);
}
.avatar {
margin-right: var(--tap-sys-spacing-4);
margin-left: var(--tap-sys-spacing-4);
}
`;

export default styles;
Loading

0 comments on commit e9b6b06

Please sign in to comment.