diff --git a/docs/dev/components/[component].md b/docs/dev/components/[component].md
index 75c93e38..b61b18e4 100644
--- a/docs/dev/components/[component].md
+++ b/docs/dev/components/[component].md
@@ -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';
diff --git a/index.html b/index.html
index 4db4c5c5..7a81a897 100644
--- a/index.html
+++ b/index.html
@@ -6,13 +6,25 @@
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
+
+
-
+
diff --git a/src/chat-bubble/chat-bubble-base.style.ts b/src/chat-bubble/chat-bubble-base.style.ts
new file mode 100644
index 00000000..043cb974
--- /dev/null
+++ b/src/chat-bubble/chat-bubble-base.style.ts
@@ -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;
diff --git a/src/chat-bubble/chat-bubble-base.ts b/src/chat-bubble/chat-bubble-base.ts
new file mode 100644
index 00000000..b0952955
--- /dev/null
+++ b/src/chat-bubble/chat-bubble-base.ts
@@ -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`
+
+ `;
+ }
+
+ 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`
+
+
+
+
+ ${this._renderFooter()}
+
+ `;
+ }
+}
diff --git a/src/chat-bubble/chat-bubble-in.style.ts b/src/chat-bubble/chat-bubble-in.style.ts
new file mode 100644
index 00000000..116002e3
--- /dev/null
+++ b/src/chat-bubble/chat-bubble-in.style.ts
@@ -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;
diff --git a/src/chat-bubble/chat-bubble-in.ts b/src/chat-bubble/chat-bubble-in.ts
new file mode 100644
index 00000000..e93f2704
--- /dev/null
+++ b/src/chat-bubble/chat-bubble-in.ts
@@ -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`
+
+ ${icon}
+
+ `;
+ }
+
+ 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`
+
+ ${icon}
+ ${stateMessage}
+
+ `;
+ }
+
+ protected override render() {
+ const rootClasses = classMap({
+ [String(this.status)]: Boolean(this.status),
+ });
+
+ return html`
+
+ ${this._renderFailureIndicator()}
+
+
+ ${this._renderStatus()}
+
+
+ `;
+ }
+}
diff --git a/src/chat-bubble/chat-bubble-out.style.ts b/src/chat-bubble/chat-bubble-out.style.ts
new file mode 100644
index 00000000..57a937e3
--- /dev/null
+++ b/src/chat-bubble/chat-bubble-out.style.ts
@@ -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;
diff --git a/src/chat-bubble/chat-bubble-out.ts b/src/chat-bubble/chat-bubble-out.ts
new file mode 100644
index 00000000..3b9a3717
--- /dev/null
+++ b/src/chat-bubble/chat-bubble-out.ts
@@ -0,0 +1,69 @@
+import "../avatar";
+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 } from "./constants";
+
+export class ChatBubbleOut extends LitElement {
+ /**
+ * The timestamp of chat element.
+ */
+ @property({ type: String })
+ public timestamp!: string;
+
+ /**
+ * The source of the avatar image.
+ */
+ @property({ type: String, attribute: "avatar-src" })
+ public avatarSrc?: string;
+
+ /**
+ * Whether or not the bubble should be fully rounded.
+ *
+ * @default false
+ */
+ @property({ type: Boolean, attribute: "fully-rounded" })
+ public fullyRounded: boolean = false;
+
+ private _renderAvatar() {
+ if (!this.avatarSrc) return nothing;
+
+ return html`
+
+
+
+ `;
+ }
+
+ protected override render() {
+ const rootClasses = classMap({
+ "has-avatar": Boolean(this.avatarSrc),
+ });
+
+ return html`
+
+ ${this._renderAvatar()}
+
+
+
+
+ `;
+ }
+}
diff --git a/src/chat-bubble/constants.ts b/src/chat-bubble/constants.ts
new file mode 100644
index 00000000..01ce58ec
--- /dev/null
+++ b/src/chat-bubble/constants.ts
@@ -0,0 +1,33 @@
+import type { TemplateResult } from "lit";
+import * as Icons from "./icons";
+
+export const AUTHORS = ["in", "out"] as const;
+export const STATES = ["sent", "seen", "pending", "failed"] as const;
+
+export const BaseSlots = {
+ BODY: "body",
+ FOOTER: "footer",
+};
+
+export const Slots = {
+ DEFAULT: "",
+};
+
+export const STATUS_TO_LOCALE_MAP: Record<
+ Exclude<(typeof STATES)[number], "failed">,
+ string
+> = {
+ pending: "در حال ارسال...",
+ seen: "خوانده شد",
+ sent: "فرستاده شد",
+};
+
+export const STATUS_TO_ICON_MAP: Record<
+ (typeof STATES)[number],
+ TemplateResult<1>
+> = {
+ failed: Icons.failed,
+ pending: Icons.pending,
+ seen: Icons.seen,
+ sent: Icons.sent,
+};
diff --git a/src/chat-bubble/icons.ts b/src/chat-bubble/icons.ts
new file mode 100644
index 00000000..2c28a738
--- /dev/null
+++ b/src/chat-bubble/icons.ts
@@ -0,0 +1,53 @@
+import { html } from "lit";
+
+export const seen = html`
+
+`;
+
+export const sent = html`
+
+`;
+
+export const pending = html`
+
+`;
+
+export const failed = html`
+
+`;
diff --git a/src/chat-bubble/index.ts b/src/chat-bubble/index.ts
new file mode 100644
index 00000000..53f524bb
--- /dev/null
+++ b/src/chat-bubble/index.ts
@@ -0,0 +1,45 @@
+import { customElement } from "lit/decorators.js";
+import { ChatBubbleIn } from "./chat-bubble-in";
+import inStyles from "./chat-bubble-in.style";
+import { ChatBubbleOut } from "./chat-bubble-out";
+import outStyles from "./chat-bubble-out.style";
+
+export { Slots } from "./constants";
+
+/**
+ * @summary Display chat-bubble-in element
+ *
+ * @prop {string} timestamp - The timestamp of chat element.
+ * @prop {"sent" | "seen" | "pending" | "failed"} [status="sent"] - The status of the chat element.
+ * @prop {boolean} [fullyRounded=false] - Whether or not the bubble should be fully rounded.
+ *
+ * @csspart [root] - The root of the element.
+ * @csspart [base] - The base-bubble element.
+ */
+@customElement("tap-chat-bubble-in")
+export class TapChatBubbleIn extends ChatBubbleIn {
+ public static override readonly styles = [inStyles];
+}
+
+/**
+ * @summary Display chat-bubble-out element
+ *
+ * @prop {string} timestamp - The timestamp of chat element.
+ * @prop {string} [avatarSrc] - The source of the avatar image.
+ * @prop {boolean} [fullyRounded=false] - Whether or not the bubble should be fully rounded.
+ *
+ * @csspart [root] - The root of the element.
+ * @csspart [avatar] - The avatar element.
+ * @csspart [base] - The base-bubble element.
+ */
+@customElement("tap-chat-bubble-out")
+export class TapChatBubbleOut extends ChatBubbleOut {
+ public static override readonly styles = [outStyles];
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "tap-chat-bubble-in": TapChatBubbleIn;
+ "tap-chat-bubble-out": TapChatBubbleOut;
+ }
+}
diff --git a/tokens/colors.css b/tokens/colors.css
index 8845a60c..942f6770 100644
--- a/tokens/colors.css
+++ b/tokens/colors.css
@@ -60,6 +60,11 @@
--tap-sys-color-brand: var(--tap-palette-orange-400);
/* gradient */
- --tap-sys-color-gradient-brand: linear-gradient(91.39deg, #ff7733 0%, #ff5722 50.15%, #e64917 100%);
+ --tap-sys-color-gradient-brand: linear-gradient(
+ 91.39deg,
+ #ff7733 0%,
+ #ff5722 50.15%,
+ #e64917 100%
+ );
/* TODO: add other gradient tokens */
}
diff --git a/tokens/typography.css b/tokens/typography.css
index 235bedc0..c5a97aab 100644
--- a/tokens/typography.css
+++ b/tokens/typography.css
@@ -81,4 +81,4 @@
--tap-sys-typography-display-lg-size: 48px;
--tap-sys-typography-display-lg-height: 72px;
--tap-sys-typography-display-lg-weight: 600;
-}
\ No newline at end of file
+}