├── .github ├── CODEOWNERS ├── husky │ ├── .gitignore │ ├── commit-msg │ └── pre-commit ├── renovate.json ├── FUNDING.yml ├── problemMatchers │ ├── tsc.json │ └── eslint.json ├── workflows │ ├── labelsync.yml │ └── continuous-integration.yml └── CONTRIBUTING.md ├── packages ├── core │ ├── .npmignore │ ├── src │ │ ├── index.ts │ │ ├── components │ │ │ ├── discord-action-row │ │ │ │ ├── discord-action-row.css │ │ │ │ ├── readme.md │ │ │ │ └── discord-action-row.tsx │ │ │ ├── discord-time │ │ │ │ ├── discord-time.css │ │ │ │ ├── readme.md │ │ │ │ └── discord-time.tsx │ │ │ ├── discord-bold │ │ │ │ ├── readme.md │ │ │ │ └── discord-bold.tsx │ │ │ ├── discord-quote │ │ │ │ ├── readme.md │ │ │ │ ├── discord-quote.tsx │ │ │ │ └── discord-quote.css │ │ │ ├── discord-italic │ │ │ │ ├── readme.md │ │ │ │ └── discord-italic.tsx │ │ │ ├── discord-spoiler │ │ │ │ ├── readme.md │ │ │ │ ├── discord-spoiler.tsx │ │ │ │ └── discord-spoiler.css │ │ │ ├── discord-reactions │ │ │ │ ├── readme.md │ │ │ │ ├── discord-reactions.css │ │ │ │ └── discord-reactions.tsx │ │ │ ├── discord-underlined │ │ │ │ ├── readme.md │ │ │ │ └── discord-underlined.tsx │ │ │ ├── discord-attachments │ │ │ │ ├── readme.md │ │ │ │ ├── discord-attachments.tsx │ │ │ │ └── discord-attachments.css │ │ │ ├── discord-embed-fields │ │ │ │ ├── readme.md │ │ │ │ ├── discord-embed-fields.css │ │ │ │ └── discord-embed-fields.tsx │ │ │ ├── discord-inline-code │ │ │ │ ├── readme.md │ │ │ │ └── discord-inline-code.tsx │ │ │ ├── discord-embed-description │ │ │ │ ├── readme.md │ │ │ │ ├── discord-embed-description.tsx │ │ │ │ └── discord-embed-description.css │ │ │ ├── svgs │ │ │ │ ├── verified-badge-overlay.tsx │ │ │ │ ├── command-icon.tsx │ │ │ │ ├── verified-tick.tsx │ │ │ │ ├── user-join.tsx │ │ │ │ ├── boost.tsx │ │ │ │ ├── system-error.tsx │ │ │ │ ├── user-leave.tsx │ │ │ │ ├── attachment-reply.tsx │ │ │ │ ├── dm-edit.tsx │ │ │ │ ├── pin.tsx │ │ │ │ ├── command-reply.tsx │ │ │ │ ├── launch-icon.tsx │ │ │ │ ├── system-alert.tsx │ │ │ │ ├── guild-badge.tsx │ │ │ │ ├── ephemeral.tsx │ │ │ │ ├── locked-voice-channel.tsx │ │ │ │ ├── voice-channel.tsx │ │ │ │ ├── reply-icon.tsx │ │ │ │ ├── dm-call.tsx │ │ │ │ ├── dm-missed-call.tsx │ │ │ │ ├── channel-icon.tsx │ │ │ │ ├── partner-badge-overlay.tsx │ │ │ │ ├── thread.tsx │ │ │ │ ├── channel-thread.tsx │ │ │ │ └── channel-forum.tsx │ │ │ ├── discord-command │ │ │ │ ├── discord-command.css │ │ │ │ ├── readme.md │ │ │ │ └── discord-command.tsx │ │ │ ├── discord-code-block │ │ │ │ ├── readme.md │ │ │ │ ├── discord-code-block.tsx │ │ │ │ └── discord-code-block.css │ │ │ ├── discord-tenor-video │ │ │ │ ├── readme.md │ │ │ │ ├── discord-tenor-video.css │ │ │ │ └── discord-tenor-video.tsx │ │ │ ├── discord-header │ │ │ │ ├── readme.md │ │ │ │ ├── discord-header.css │ │ │ │ └── discord-header.tsx │ │ │ ├── discord-custom-emoji │ │ │ │ ├── discord-custom-emoji.css │ │ │ │ ├── readme.md │ │ │ │ └── discord-custom-emoji.tsx │ │ │ ├── discord-messages │ │ │ │ ├── readme.md │ │ │ │ ├── discord-messages.tsx │ │ │ │ └── discord-messages.css │ │ │ ├── discord-thread │ │ │ │ ├── readme.md │ │ │ │ ├── discord-thread.tsx │ │ │ │ └── discord-thread.css │ │ │ ├── discord-embed-footer │ │ │ │ ├── discord-embed-footer.css │ │ │ │ ├── readme.md │ │ │ │ └── discord-embed-footer.tsx │ │ │ ├── discord-embed-field │ │ │ │ ├── readme.md │ │ │ │ ├── discord-embed-field.css │ │ │ │ └── discord-embed-field.tsx │ │ │ ├── discord-reaction │ │ │ │ ├── readme.md │ │ │ │ ├── discord-reaction.css │ │ │ │ └── discord-reaction.tsx │ │ │ ├── discord-mention │ │ │ │ ├── discord-mention.css │ │ │ │ ├── readme.md │ │ │ │ └── discord-mention.tsx │ │ │ ├── discord-attachment │ │ │ │ ├── readme.md │ │ │ │ ├── discord-attachment.css │ │ │ │ └── discord-attachment.tsx │ │ │ ├── discord-button │ │ │ │ ├── discord-button.css │ │ │ │ ├── readme.md │ │ │ │ └── discord-button.tsx │ │ │ ├── discord-system-message │ │ │ │ ├── readme.md │ │ │ │ ├── discord-system-message.css │ │ │ │ └── discord-system-message.tsx │ │ │ ├── discord-thread-message │ │ │ │ ├── discord-thread-message.css │ │ │ │ ├── readme.md │ │ │ │ └── discord-thread-message.tsx │ │ │ ├── author-info │ │ │ │ ├── author-info.css │ │ │ │ └── author-info.tsx │ │ │ ├── discord-invite │ │ │ │ ├── readme.md │ │ │ │ ├── discord-invite.tsx │ │ │ │ └── discord-invite.css │ │ │ ├── discord-embed │ │ │ │ ├── readme.md │ │ │ │ └── discord-embed.css │ │ │ ├── discord-reply │ │ │ │ ├── readme.md │ │ │ │ ├── discord-reply.css │ │ │ │ └── discord-reply.tsx │ │ │ └── discord-message │ │ │ │ └── readme.md │ │ ├── Fragment.tsx │ │ ├── util.ts │ │ └── options.ts │ ├── static │ │ ├── tcd.png │ │ ├── avafive.png │ │ ├── avafour.png │ │ ├── avaone.png │ │ ├── avatwo.png │ │ ├── booster.png │ │ ├── diamond.png │ │ ├── pikawow.png │ │ ├── skyra.png │ │ ├── stencil.png │ │ ├── avathree.png │ │ ├── blobparty.gif │ │ ├── discordjs.png │ │ ├── dragonite.png │ │ ├── sapphire.png │ │ ├── aichansmile.png │ │ ├── skyralounge.gif │ │ ├── eyes.svg │ │ └── thumbsup.svg │ ├── tsconfig.json │ ├── scripts │ │ └── replaceImportInPolyfills.cjs │ ├── stencil.config.ts │ └── package.json └── react │ ├── src │ ├── react-component-lib │ │ ├── index.ts │ │ ├── utils │ │ │ ├── case.ts │ │ │ ├── dev.ts │ │ │ ├── index.tsx │ │ │ └── attachProps.ts │ │ ├── interfaces.ts │ │ ├── createComponent.tsx │ │ └── createOverlayComponent.tsx │ └── index.ts │ ├── scripts │ ├── clean.mjs │ └── replaceDefaultMod.cjs │ ├── tsconfig.json │ └── package.json ├── .prettierignore ├── assets ├── fonts │ ├── Bold.woff │ ├── Book.woff │ ├── Medium.woff │ └── Semibold.woff ├── dark_mode │ ├── with_embed.png │ ├── compact_mode.png │ └── normal_conversation.png └── light_mode │ ├── with_embed.png │ ├── compact_mode.png │ └── normal_conversation.png ├── tsconfig.eslint.json ├── .eslintignore ├── tsconfig.base.json ├── .yarnrc.yml ├── .yarn └── patches │ └── @sapphire-ts-config-npm-4.0.0-cfd20d4fc5.patch ├── lerna.json ├── .gitignore ├── scripts └── clean.mjs ├── LICENSE.md ├── package.json └── .eslintrc /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @favna -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './options'; 3 | -------------------------------------------------------------------------------- /.github/husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | loader/ 3 | node_modules/ 4 | www/ 5 | *.png 6 | packages/core/src/components.d.ts -------------------------------------------------------------------------------- /assets/fonts/Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/fonts/Bold.woff -------------------------------------------------------------------------------- /assets/fonts/Book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/fonts/Book.woff -------------------------------------------------------------------------------- /assets/fonts/Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/fonts/Medium.woff -------------------------------------------------------------------------------- /assets/fonts/Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/fonts/Semibold.woff -------------------------------------------------------------------------------- /packages/core/static/tcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/tcd.png -------------------------------------------------------------------------------- /.github/husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn pretty-quick --staged && yarn lint-staged -------------------------------------------------------------------------------- /assets/dark_mode/with_embed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/dark_mode/with_embed.png -------------------------------------------------------------------------------- /assets/light_mode/with_embed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/light_mode/with_embed.png -------------------------------------------------------------------------------- /packages/core/static/avafive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/avafive.png -------------------------------------------------------------------------------- /packages/core/static/avafour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/avafour.png -------------------------------------------------------------------------------- /packages/core/static/avaone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/avaone.png -------------------------------------------------------------------------------- /packages/core/static/avatwo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/avatwo.png -------------------------------------------------------------------------------- /packages/core/static/booster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/booster.png -------------------------------------------------------------------------------- /packages/core/static/diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/diamond.png -------------------------------------------------------------------------------- /packages/core/static/pikawow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/pikawow.png -------------------------------------------------------------------------------- /packages/core/static/skyra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/skyra.png -------------------------------------------------------------------------------- /packages/core/static/stencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/stencil.png -------------------------------------------------------------------------------- /assets/dark_mode/compact_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/dark_mode/compact_mode.png -------------------------------------------------------------------------------- /assets/light_mode/compact_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/light_mode/compact_mode.png -------------------------------------------------------------------------------- /packages/core/static/avathree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/avathree.png -------------------------------------------------------------------------------- /packages/core/static/blobparty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/blobparty.gif -------------------------------------------------------------------------------- /packages/core/static/discordjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/discordjs.png -------------------------------------------------------------------------------- /packages/core/static/dragonite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/dragonite.png -------------------------------------------------------------------------------- /packages/core/static/sapphire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/sapphire.png -------------------------------------------------------------------------------- /packages/core/static/aichansmile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/aichansmile.png -------------------------------------------------------------------------------- /packages/core/static/skyralounge.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/packages/core/static/skyralounge.gif -------------------------------------------------------------------------------- /assets/dark_mode/normal_conversation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/dark_mode/normal_conversation.png -------------------------------------------------------------------------------- /assets/light_mode/normal_conversation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItzDerock/discord-components/HEAD/assets/light_mode/normal_conversation.png -------------------------------------------------------------------------------- /packages/core/src/components/discord-action-row/discord-action-row.css: -------------------------------------------------------------------------------- 1 | .discord-action-row { 2 | display: flex; 3 | flex-wrap: nowrap; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-time/discord-time.css: -------------------------------------------------------------------------------- 1 | .discord-time { 2 | background-color: #ffffff0f; 3 | border-radius: 3px; 4 | padding: 0 2px; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-bold/readme.md: -------------------------------------------------------------------------------- 1 | # discord-bold 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-quote/readme.md: -------------------------------------------------------------------------------- 1 | # discord-quote 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/react/src/react-component-lib/index.ts: -------------------------------------------------------------------------------- 1 | export { createReactComponent } from './createComponent'; 2 | export { createOverlayComponent } from './createOverlayComponent'; 3 | -------------------------------------------------------------------------------- /packages/core/src/Fragment.tsx: -------------------------------------------------------------------------------- 1 | import type { VNode } from '@stencil/core'; 2 | 3 | export default function Fragment(_props: T, children: VNode[]) { 4 | return [...children]; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-italic/readme.md: -------------------------------------------------------------------------------- 1 | # discord-italic 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-spoiler/readme.md: -------------------------------------------------------------------------------- 1 | # discord-spoiler 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "enabled": false, 4 | "extends": ["github>sapphiredev/readme:sapphire-renovate"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-action-row/readme.md: -------------------------------------------------------------------------------- 1 | # discord-action-row 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reactions/readme.md: -------------------------------------------------------------------------------- 1 | # discord-reactions 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-underlined/readme.md: -------------------------------------------------------------------------------- 1 | # discord-underlined 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-attachments/readme.md: -------------------------------------------------------------------------------- 1 | # discord-attachments 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-fields/readme.md: -------------------------------------------------------------------------------- 1 | # discord-embed-fields 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-inline-code/readme.md: -------------------------------------------------------------------------------- 1 | # discord-inline-code 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-description/readme.md: -------------------------------------------------------------------------------- 1 | # discord-embed-description 2 | 3 | 4 | 5 | --- 6 | 7 | _Built with [StencilJS](https://stenciljs.com/)_ 8 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-fields/discord-embed-fields.css: -------------------------------------------------------------------------------- 1 | .discord-embed .discord-embed-fields { 2 | display: grid; 3 | grid-column: 1/1; 4 | margin-top: 8px; 5 | grid-gap: 8px; 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["packages/", "scripts/"], 4 | "exclude": ["dist/", "node_modules/", "www/", "loader/", "packages/**/scripts/*.cjs"] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.jsx 3 | *.test.js 4 | *.test.jsx 5 | stencil-public-runtime.d.ts 6 | packages/core/dist 7 | packages/core/loader 8 | packages/core/www 9 | packages/core/src/components.d.ts 10 | node_modules/ -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "jsxFactory": "h", 7 | "experimentalDecorators": true 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/react/scripts/clean.mjs: -------------------------------------------------------------------------------- 1 | import { rm } from 'node:fs/promises'; 2 | 3 | const distFolder = new URL('../dist', import.meta.url); 4 | 5 | const options = { recursive: true, force: true }; 6 | 7 | await Promise.all([ 8 | rm(distFolder, options) // 9 | ]); 10 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reactions/discord-reactions.css: -------------------------------------------------------------------------------- 1 | .discord-message .discord-reactions, 2 | .discord-system-message .discord-reactions { 3 | display: flex; 4 | -webkit-box-flex: 1; 5 | -ms-flex: 1 0 auto; 6 | flex: 1 0 auto; 7 | align-items: center; 8 | flex-wrap: wrap; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-bold/discord-bold.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-bold' 5 | }) 6 | export class DiscordBold { 7 | public render() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-italic/discord-italic.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-italic' 5 | }) 6 | export class DiscordItalic { 7 | public render() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-underlined/discord-underlined.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-underlined' 5 | }) 6 | export class DiscordUnderlined { 7 | public render() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-inline-code/discord-inline-code.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-inline-code' 5 | }) 6 | export class DiscordInlineCode { 7 | public render() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "module": "CommonJS", 7 | "tsBuildInfoFile": "dist/.tsbuildinfo", 8 | "importsNotUsedAsValues": "remove", 9 | "declarationMap": true 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/react/scripts/replaceDefaultMod.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/unbound-method, @typescript-eslint/no-var-requires 2 | const { resolve } = require('path'); 3 | 4 | module.exports = { 5 | from: 'export default mod;\n', 6 | to: '', 7 | files: [resolve(__dirname, '../dist/index.mjs')], 8 | quiet: true 9 | }; 10 | -------------------------------------------------------------------------------- /packages/react/src/react-component-lib/utils/case.ts: -------------------------------------------------------------------------------- 1 | export const dashToPascalCase = (str: string) => 2 | str 3 | .toLowerCase() 4 | .split('-') 5 | .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) 6 | .join(''); 7 | export const camelToDashCase = (str: string) => str.replace(/([A-Z])/g, (m: string) => `-${m[0].toLowerCase()}`); 8 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sapphire/ts-config", 3 | "compilerOptions": { 4 | "lib": ["ESNext", "DOM"], 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "target": "ES2017", 8 | "jsx": "react", 9 | "strictPropertyInitialization": false, 10 | "useDefineForClassFields": false, 11 | "declarationMap": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/verified-badge-overlay.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | const VerifiedBadgeOverlay = () => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default VerifiedBadgeOverlay; 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kyranet, favna] 4 | patreon: kyranet 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: kyranet 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: ['https://donate.skyra.pw/paypal', 'https://donate.favware.tech/paypal'] 9 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/command-icon.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function CommandIcon(props: T) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/verified-tick.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | const VerifiedTick = () => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default VerifiedTick; 10 | -------------------------------------------------------------------------------- /.github/problemMatchers/tsc.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "tsc", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\s+\\d+\\>)?([^\\s].*)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\)\\s*:\\s+(error|warning|info)\\s+(\\w{1,2}\\d+)\\s*:\\s*(.*)$", 8 | "file": 1, 9 | "location": 2, 10 | "severity": 3, 11 | "code": 4, 12 | "message": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/react/src/react-component-lib/utils/dev.ts: -------------------------------------------------------------------------------- 1 | export const isDevMode = () => { 2 | return process && process.env && process.env.NODE_ENV === 'development'; 3 | }; 4 | 5 | const warnings: { [key: string]: boolean } = {}; 6 | 7 | export const deprecationWarning = (key: string, message: string) => { 8 | if (isDevMode()) { 9 | if (!warnings[key]) { 10 | console.warn(message); 11 | warnings[key] = true; 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reactions/discord-reactions.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, h, Host } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-reactions', 5 | styleUrl: 'discord-reactions.css' 6 | }) 7 | export class DiscordReactions implements ComponentInterface { 8 | public render() { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/user-join.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function UserJoin(props: T) { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-action-row/discord-action-row.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, h, Host } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-action-row', 5 | styleUrl: 'discord-action-row.css' 6 | }) 7 | export class DiscordActionRow implements ComponentInterface { 8 | public render() { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-attachments/discord-attachments.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, h, Host } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-attachments', 5 | styleUrl: 'discord-attachments.css' 6 | }) 7 | export class DiscordAttachments implements ComponentInterface { 8 | public render() { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/boost.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function Boost(props: T) { 4 | return ( 5 | 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-fields/discord-embed-fields.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, h, Host } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-embed-fields', 5 | styleUrl: 'discord-embed-fields.css' 6 | }) 7 | export class DiscordEmbedFields implements ComponentInterface { 8 | public render() { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | 3 | nodeLinker: node-modules 4 | 5 | plugins: 6 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 7 | spec: '@yarnpkg/plugin-interactive-tools' 8 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 9 | spec: '@yarnpkg/plugin-typescript' 10 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 11 | spec: '@yarnpkg/plugin-workspace-tools' 12 | 13 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 14 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/system-error.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function SystemError(props: T) { 4 | return ( 5 | 6 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.github/problemMatchers/eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "eslint-stylish", 5 | "pattern": [ 6 | { 7 | "regexp": "^([^\\s].*)$", 8 | "file": 1 9 | }, 10 | { 11 | "regexp": "^\\s+(\\d+):(\\d+)\\s+(error|warning|info)\\s+(.*)\\s\\s+(.*)$", 12 | "line": 1, 13 | "column": 2, 14 | "severity": 3, 15 | "message": 4, 16 | "code": 5, 17 | "loop": true 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/user-leave.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function UserLeave(props: T) { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-quote/discord-quote.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h, Host } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-quote', 5 | styleUrl: 'discord-quote.css' 6 | }) 7 | export class DiscordQuote { 8 | public render() { 9 | return ( 10 | 11 |
12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-attachments/discord-attachments.css: -------------------------------------------------------------------------------- 1 | .discord-message .discord-attachments { 2 | display: grid; 3 | grid-auto-flow: row; 4 | grid-row-gap: 0.25rem; 5 | text-indent: 0; 6 | min-height: 0; 7 | min-width: 0; 8 | padding-top: 0.125rem; 9 | padding-bottom: 0.125rem; 10 | position: relative; 11 | } 12 | 13 | .discord-message .discord-attachments > * { 14 | justify-self: start; 15 | -ms-flex-item-align: start; 16 | align-self: start; 17 | } 18 | -------------------------------------------------------------------------------- /.yarn/patches/@sapphire-ts-config-npm-4.0.0-cfd20d4fc5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/tsconfig.json b/tsconfig.json 2 | index 1e6ea62bb5dd607fc9b41e5c5b258830cc25aff7..35079a33f48174d564d2bdfb28701136b0e58072 100644 3 | --- a/tsconfig.json 4 | +++ b/tsconfig.json 5 | @@ -9,7 +9,6 @@ 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "importHelpers": true, 9 | - "verbatimModuleSyntax": true, 10 | "incremental": true, 11 | "lib": ["esnext"], 12 | "module": "Node16", 13 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/lerna", 3 | "packages": ["packages/*"], 4 | "npmClient": "yarn", 5 | "useWorkspaces": true, 6 | "version": "3.6.1", 7 | "command": { 8 | "publish": { 9 | "conventionalCommits": true, 10 | "push": true 11 | }, 12 | "version": { 13 | "allowBranch": "main", 14 | "conventionalCommits": true, 15 | "push": true, 16 | "createRelease": "github", 17 | "signGitCommit": true, 18 | "signGitTag": true 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-command/discord-command.css: -------------------------------------------------------------------------------- 1 | .discord-replied-message.discord-executed-command .discord-command-name { 2 | color: #00aff4; 3 | font-weight: 500; 4 | } 5 | 6 | .discord-replied-message.discord-executed-command .discord-command-name:hover { 7 | color: #00aff4; 8 | text-decoration: underline; 9 | } 10 | 11 | .discord-replied-message.discord-executed-command .discord-replied-message-username { 12 | margin-right: 0; 13 | } 14 | 15 | @import '../discord-reply/discord-reply.css'; 16 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/attachment-reply.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function AttachmentReply(props: T) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/dm-edit.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function DMEdit(props: T) { 4 | return ( 5 | 6 | 7 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/pin.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function Pin(props: T) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/scripts/replaceImportInPolyfills.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/unbound-method, @typescript-eslint/no-var-requires 2 | const { resolve } = require('path'); 3 | 4 | module.exports = { 5 | from: /import\(/g, 6 | to: 'import(\n/* @vite-ignore */\n', 7 | files: [ 8 | resolve(__dirname, '../dist/esm/index*.js'), 9 | resolve(__dirname, '../dist/esm/polyfills/index.js'), 10 | resolve(__dirname, '../dist/esm/polyfills/system.js'), 11 | resolve(__dirname, '../dist/derockdev-discord-components-core/p*.js') 12 | ], 13 | quiet: true 14 | }; 15 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-spoiler/discord-spoiler.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h, Host, Listen, State } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-spoiler', 5 | styleUrl: 'discord-spoiler.css' 6 | }) 7 | export class DiscordSpoiler { 8 | @State() private isRevealed = false; 9 | 10 | @Listen('click') 11 | public reveal() { 12 | this.isRevealed = true; 13 | } 14 | 15 | public render() { 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/command-reply.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function CommandReply(props: T) { 4 | return ( 5 | 6 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/launch-icon.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function LaunchIcon(props: T) { 4 | return ( 5 | 6 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/labelsync.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Label Sync 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | label_sync: 10 | name: Automatic Label Synchronization 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Project 14 | uses: actions/checkout@v3 15 | with: 16 | repository: 'sapphiredev/readme' 17 | - name: Run Label Sync 18 | uses: crazy-max/ghaction-github-labeler@v4 19 | with: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} 21 | yaml-file: .github/labels.yml 22 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-code-block/readme.md: -------------------------------------------------------------------------------- 1 | # discord-channel-header 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ---------- | ---------- | ------------------------------- | --------------------- | ----------- | 9 | | `code` | `code` | The code to display. | `string` | `undefined` | 10 | | `language` | `language` | The language of the code block. | `string \| undefined` | `undefined` | 11 | 12 | --- 13 | 14 | _Built with [StencilJS](https://stenciljs.com/)_ 15 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/system-alert.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function SystemAlert(props: T) { 4 | return ( 5 | 6 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | /.pnp 4 | .pnp.js 5 | package-lock.json 6 | 7 | # IDE config 8 | .idea/ 9 | .vscode/ 10 | 11 | # Yarn files 12 | .yarn/install-state.gz 13 | .yarn/build-state.yml 14 | 15 | # Output 16 | dist/ 17 | hydrate/ 18 | docs/ 19 | build-scripts/ 20 | build/ 21 | www/ 22 | loader/ 23 | *.tsbuildinfo 24 | *.js 25 | *.jsx 26 | *.js.map 27 | *.jsx.map 28 | *.tgz 29 | !rollup.config.js 30 | 31 | # Misc 32 | *~ 33 | *.sw[mnpcod] 34 | *.log 35 | *.tmp 36 | *.tmp.* 37 | log.txt 38 | .sass-cache/ 39 | .versions/ 40 | .stencil/ 41 | $RECYCLE.BIN/ 42 | .DS_Store 43 | Thumbs.db 44 | UserInterfaceState.xcuserstate 45 | .env 46 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-quote/discord-quote.css: -------------------------------------------------------------------------------- 1 | .discord-message .discord-message-body .discord-quote-container { 2 | display: flex; 3 | } 4 | 5 | .discord-message .discord-message-body .discord-quote-container > .discord-quote-divider { 6 | background-color: #4f545c; 7 | border-radius: 4px; 8 | font-size: 0.9em; 9 | font-style: normal; 10 | font-weight: 400; 11 | margin: 0; 12 | padding: 0; 13 | width: 4px; 14 | } 15 | 16 | .discord-message .discord-message-body blockquote { 17 | margin-block-end: unset; 18 | margin-block-start: unset; 19 | margin-inline-end: unset; 20 | margin-inline-start: unset; 21 | padding: 0 8px 0 12px; 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-tenor-video/readme.md: -------------------------------------------------------------------------------- 1 | # discord-attachment 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | -------- | --------- | --------------------------------- | -------- | ----------- | 9 | | `height` | `height` | The height of the video in pixels | `number` | `undefined` | 10 | | `url` | `url` | The URL for the video | `string` | `undefined` | 11 | | `width` | `width` | The width of the video in pixels | `number` | `undefined` | 12 | 13 | --- 14 | 15 | _Built with [StencilJS](https://stenciljs.com/)_ 16 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-header/readme.md: -------------------------------------------------------------------------------- 1 | # discord-code-block 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | --------- | --------- | ----------------------- | --------------------- | ----------- | 9 | | `channel` | `channel` | The name of the channel | `string` | `undefined` | 10 | | `guild` | `guild` | The guild name | `string` | `undefined` | 11 | | `icon` | `icon` | The icon to display. | `string \| undefined` | `undefined` | 12 | 13 | --- 14 | 15 | _Built with [StencilJS](https://stenciljs.com/)_ 16 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-custom-emoji/discord-custom-emoji.css: -------------------------------------------------------------------------------- 1 | .discord-custom-emoji { 2 | display: inline-block; 3 | cursor: pointer; 4 | vertical-align: bottom; 5 | } 6 | 7 | .discord-custom-emoji .discord-custom-emoji-image { 8 | object-fit: contain; 9 | width: 1.375rem; 10 | height: 1.375rem; 11 | vertical-align: bottom; 12 | } 13 | 14 | .discord-custom-emoji .discord-custom-emoji-image-large { 15 | width: 2.8em; 16 | height: 2.8em; 17 | } 18 | 19 | .discord-embed-custom-emoji { 20 | display: inline-block; 21 | } 22 | 23 | .discord-embed-custom-emoji .discord-embed-custom-emoji-image { 24 | width: 18px; 25 | height: 18px; 26 | vertical-align: bottom; 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-time/readme.md: -------------------------------------------------------------------------------- 1 | # discord-time 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ----------- | ----------- | ------------------------ | ----------------------------------------------- | ----------- | 9 | | `format` | `format` | The format for the time. | `"D" \| "F" \| "R" \| "T" \| "d" \| "f" \| "t"` | `'t'` | 10 | | `timestamp` | `timestamp` | The time to display. | `number` | `undefined` | 11 | 12 | --- 13 | 14 | _Built with [StencilJS](https://stenciljs.com/)_ 15 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/guild-badge.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function GuildBadge(props: T) { 4 | return ( 5 | 6 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/ephemeral.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function Ephemeral(props: T) { 4 | return ( 5 | 6 | 10 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/locked-voice-channel.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function LockedVoiceChannel(props: T) { 4 | return ( 5 | 6 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-messages/readme.md: -------------------------------------------------------------------------------- 1 | # discord-messages 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | -------------- | --------------- | ----------------------------------------- | --------- | ----------- | 9 | | `compactMode` | `compact-mode` | Whether to use compact mode or not. | `boolean` | `undefined` | 10 | | `lightTheme` | `light-theme` | Whether to use light theme or not. | `boolean` | `undefined` | 11 | | `noBackground` | `no-background` | Whether to exclude the background or not. | `boolean` | `undefined` | 12 | 13 | --- 14 | 15 | _Built with [StencilJS](https://stenciljs.com/)_ 16 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-thread/readme.md: -------------------------------------------------------------------------------- 1 | # discord-thread 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | -------- | --------- | -------------------------------------------------------------------------------- | -------- | -------------- | 9 | | `cta` | `cta` | The the text within the call to action text. (i.e. 'See Thread' or 'x Messages') | `string` | `'See Thread'` | 10 | | `name` | `name` | The name of the thread. | `string` | `'Thread'` | 11 | 12 | --- 13 | 14 | _Built with [StencilJS](https://stenciljs.com/)_ 15 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-footer/discord-embed-footer.css: -------------------------------------------------------------------------------- 1 | .discord-embed-footer { 2 | -webkit-box-align: center; 3 | align-items: center; 4 | color: #dcddde; 5 | display: flex; 6 | font-size: 12px; 7 | line-height: 16px; 8 | font-weight: 500; 9 | grid-column: 1/3; 10 | grid-row: auto/auto; 11 | margin-top: 8px; 12 | } 13 | 14 | .discord-embed-footer .discord-footer-image { 15 | border-radius: 50%; 16 | flex-shrink: 0; 17 | height: 20px; 18 | margin-right: 8px; 19 | width: 20px; 20 | } 21 | 22 | .discord-embed-footer .discord-footer-separator { 23 | color: #dcddde; 24 | font-weight: 500; 25 | display: inline-block; 26 | margin: 0 4px; 27 | } 28 | 29 | .discord-light-theme .discord-embed-footer .discord-footer-separator { 30 | color: #e4e4e4; 31 | } 32 | -------------------------------------------------------------------------------- /scripts/clean.mjs: -------------------------------------------------------------------------------- 1 | import { rm } from 'node:fs/promises'; 2 | 3 | const coreDirectory = new URL('../packages/core/', import.meta.url); 4 | const reactDirectory = new URL('../packages/react/', import.meta.url); 5 | 6 | const coreDistDirectory = new URL('dist/', coreDirectory); 7 | const coreLoaderDirectory = new URL('loader/', coreDirectory); 8 | const coreWwwDirectory = new URL('www/', coreDirectory); 9 | 10 | const reactSrcDirectory = new URL('src/', reactDirectory); 11 | const reactDistDirectory = new URL('dist/', reactDirectory); 12 | 13 | const options = { recursive: true, force: true }; 14 | 15 | await Promise.all([ 16 | rm(coreDistDirectory, options), 17 | rm(coreLoaderDirectory, options), 18 | rm(coreWwwDirectory, options), 19 | rm(reactSrcDirectory, options), 20 | rm(reactDistDirectory, options) 21 | ]); 22 | -------------------------------------------------------------------------------- /packages/core/src/util.ts: -------------------------------------------------------------------------------- 1 | import type { Emoji } from './options'; 2 | 3 | export type DiscordTimestamp = Date | string | null; 4 | 5 | export const handleTimestamp = (value: DiscordTimestamp, hour24 = false): string | null => { 6 | if (!(value instanceof Date) && typeof value !== 'string') { 7 | throw new TypeError('Timestamp prop must be a Date object or a string.'); 8 | } 9 | 10 | const date = new Date(value) 11 | .toLocaleDateString(undefined, { 12 | year: 'numeric', 13 | month: '2-digit', 14 | day: '2-digit', 15 | hour: '2-digit', 16 | minute: '2-digit', 17 | hour12: !hour24 18 | }) 19 | .replace(',', ''); 20 | 21 | return hour24 ? date : date.replace(/00:(\d{2})/, '12:$1'); 22 | }; 23 | 24 | export const getGlobalEmojiUrl = (emojiName: string): Emoji | undefined => window.$discordMessage?.emojis?.[emojiName]; 25 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **The issue tracker is only for issue reporting or proposals/suggestions. If you have a question, you can find us in our [Discord Server]**. 4 | 5 | To contribute to this repository, feel free to create a new fork of the repository and 6 | submit a pull request. We highly suggest [ESLint] to be installed 7 | in your text editor or IDE of your choice to ensure builds from GitHub Actions do not fail. 8 | 9 | 1. Fork, clone, and select the **main** branch. 10 | 2. Create a new branch in your fork. 11 | 3. Make your changes. 12 | 4. Ensure your linting and tests pass by running `yarn lint` 13 | 5. Commit your changes, and push them. 14 | 6. Submit a Pull Request [here]! 15 | 16 | [discord server]: https://join.skyra.pw 17 | [here]: https://github.com/skyra-project/discord-components/pulls 18 | [eslint]: https://eslint.org/ 19 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-header/discord-header.css: -------------------------------------------------------------------------------- 1 | .discord-header { 2 | display: flex; 3 | flex-direction: row; 4 | max-height: 5rem; 5 | padding: 0.5rem; 6 | gap: 0.5rem; 7 | border-bottom: 1px solid rgba(79, 84, 92, 0.48); 8 | } 9 | 10 | .discord-header-icon { 11 | float: left; 12 | width: 5rem; 13 | } 14 | 15 | .discord-header-icon > div { 16 | background-color: rgb(79, 84, 92); 17 | border-radius: 50%; 18 | width: 5rem; 19 | height: 5rem; 20 | text-align: center; 21 | align-items: center; 22 | justify-content: center; 23 | display: flex; 24 | font-size: xx-large; 25 | } 26 | 27 | .discord-header-icon > img { 28 | border-radius: 50%; 29 | width: auto; 30 | height: 100%; 31 | } 32 | 33 | .discord-header-text { 34 | flex-grow: 1; 35 | } 36 | 37 | .discord-header-text-guild { 38 | font-size: 1.5rem; 39 | font-weight: bold; 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-tenor-video/discord-tenor-video.css: -------------------------------------------------------------------------------- 1 | .discord-tenor-video { 2 | color: #dcddde; 3 | display: flex; 4 | font-size: 13px; 5 | line-height: 150%; 6 | margin-bottom: 8px; 7 | margin-top: 8px; 8 | } 9 | 10 | .discord-tenor-video .discord-tenor-video-wrapper { 11 | display: block; 12 | position: relative; 13 | -webkit-user-select: text; 14 | -moz-user-select: text; 15 | -ms-user-select: text; 16 | user-select: text; 17 | overflow: hidden; 18 | border-radius: 4px; 19 | } 20 | 21 | .discord-tenor-video .discord-tenor-video-wrapper video { 22 | -webkit-box-align: center; 23 | -webkit-box-pack: center; 24 | align-items: center; 25 | border-radius: 0; 26 | cursor: pointer; 27 | display: flex; 28 | height: 100%; 29 | justify-content: center; 30 | max-height: 100%; 31 | width: 100%; 32 | left: 0px; 33 | top: 0px; 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-field/readme.md: -------------------------------------------------------------------------------- 1 | # discord-embed-field 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------------------- | -------------- | ----------------------------------------------------- | --------- | ----------- | 9 | | `fieldTitle` _(required)_ | `field-title` | The field's title. | `string` | `undefined` | 10 | | `inline` | `inline` | Whether this field should be displayed inline or not. | `boolean` | `false` | 11 | | `inlineIndex` | `inline-index` | The index of this inline field | `number` | `1` | 12 | 13 | --- 14 | 15 | _Built with [StencilJS](https://stenciljs.com/)_ 16 | -------------------------------------------------------------------------------- /packages/core/stencil.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@stencil/core'; 2 | import { reactOutputTarget } from '@stencil/react-output-target'; 3 | 4 | export const config: Config = { 5 | namespace: 'derockdev-discord-components-core', 6 | extras: { 7 | experimentalImportInjection: true 8 | }, 9 | outputTargets: [ 10 | reactOutputTarget({ 11 | componentCorePackage: '@derockdev/discord-components-core', 12 | proxiesFile: '../react/src/index.ts', 13 | includeDefineCustomElements: true, 14 | includePolyfills: false 15 | }), 16 | { 17 | type: 'dist', 18 | empty: true, 19 | esmLoaderPath: '../loader' 20 | }, 21 | { 22 | type: 'docs-readme', 23 | strict: true 24 | }, 25 | { 26 | type: 'www', 27 | serviceWorker: null, 28 | copy: [{ src: '../static', dest: 'static/' }] 29 | }, 30 | { type: 'dist-hydrate-script' } 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-description/discord-embed-description.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-embed-description', 5 | styleUrl: 'discord-embed-description.css' 6 | }) 7 | export class DiscordEmbedDescription implements ComponentInterface { 8 | /** 9 | * The DiscordEmbedDescription element. 10 | */ 11 | @Element() 12 | public el: HTMLElement; 13 | 14 | public render() { 15 | const parent: HTMLDiscordMessagesElement = this.el.parentElement as HTMLDiscordMessagesElement; 16 | 17 | if (parent.tagName.toLowerCase() !== 'div') { 18 | throw new Error('All components must be direct children of .'); 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-spoiler/discord-spoiler.css: -------------------------------------------------------------------------------- 1 | .discord-message .discord-message-body .discord-spoiler { 2 | background-color: #202225; 3 | color: transparent; 4 | cursor: pointer; 5 | } 6 | 7 | .discord-light-theme .discord-message .discord-message-body .discord-spoiler { 8 | background-color: #b9bbbe; 9 | } 10 | 11 | .discord-message .discord-message-body .discord-spoiler:hover { 12 | background-color: rgba(32, 34, 37, 0.8); 13 | } 14 | 15 | .discord-light-theme .discord-message .discord-message-body .discord-spoiler:hover { 16 | background-color: rgba(185, 187, 190, 0.8); 17 | } 18 | 19 | .discord-message .discord-message-body .discord-spoiler--revealed { 20 | color: inherit; 21 | background-color: hsla(0, 0%, 100%, 0.1); 22 | } 23 | 24 | .discord-light-theme .discord-message .discord-message-body .discord-spoiler--revealed { 25 | background-color: rgba(0, 0, 0, 0.1); 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-field/discord-embed-field.css: -------------------------------------------------------------------------------- 1 | .discord-embed .discord-embed-field { 2 | font-size: 0.875rem; 3 | line-height: 1.125rem; 4 | min-width: 0; 5 | font-weight: 400; 6 | grid-column: 1/13; 7 | } 8 | 9 | .discord-embed .discord-embed-field .discord-field-title { 10 | color: #ffffff; 11 | font-weight: 600; 12 | font-size: 0.875rem; 13 | line-height: 1.125rem; 14 | min-width: 0; 15 | margin-bottom: 2px; 16 | } 17 | 18 | .discord-embed .discord-embed-field.discord-inline-field { 19 | flex-grow: 1; 20 | flex-basis: auto; 21 | min-width: 150px; 22 | } 23 | 24 | .discord-light-theme .discord-embed .discord-embed-field .discord-field-title { 25 | color: #747f8d; 26 | } 27 | 28 | .discord-embed-inline-field-3 { 29 | grid-column: 9/13 !important; 30 | } 31 | 32 | .discord-embed-inline-field-2 { 33 | grid-column: 5/9 !important; 34 | } 35 | 36 | .discord-embed-inline-field-1 { 37 | grid-column: 1/5 !important; 38 | } 39 | -------------------------------------------------------------------------------- /packages/react/src/react-component-lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | // General types important to applications using stencil built components 2 | export interface EventEmitter { 3 | emit: (data?: T) => CustomEvent; 4 | } 5 | 6 | export interface StyleReactProps { 7 | class?: string; 8 | className?: string; 9 | style?: { [key: string]: any }; 10 | } 11 | 12 | export interface OverlayEventDetail { 13 | data?: T; 14 | role?: string; 15 | } 16 | 17 | export interface OverlayInterface { 18 | el: HTMLElement; 19 | animated: boolean; 20 | keyboardClose: boolean; 21 | overlayIndex: number; 22 | presented: boolean; 23 | 24 | enterAnimation?: any; 25 | leaveAnimation?: any; 26 | 27 | didPresent: EventEmitter; 28 | willPresent: EventEmitter; 29 | willDismiss: EventEmitter; 30 | didDismiss: EventEmitter; 31 | 32 | present(): Promise; 33 | dismiss(data?: any, role?: string): Promise; 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-code-block/discord-code-block.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Host, h, Prop, ComponentInterface } from '@stencil/core'; 2 | import hljs from 'highlight.js'; 3 | 4 | @Component({ 5 | tag: 'discord-code-block', 6 | styleUrl: 'discord-code-block.css' 7 | }) 8 | export class DiscordCodeBlock implements ComponentInterface { 9 | /** 10 | * The language of the code block. 11 | */ 12 | @Prop() 13 | public language?: string; 14 | 15 | /** 16 | * The code to display. 17 | */ 18 | @Prop() 19 | public code: string; 20 | 21 | public render() { 22 | // check if hljs has the language 23 | const language = this.language ? (hljs.getLanguage(this.language) ? this.language : 'plaintext') : 'plaintext'; 24 | 25 | return ( 26 | 27 | 28 | 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-footer/readme.md: -------------------------------------------------------------------------------- 1 | # discord-embed-footer 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------- | -------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------- | ----------- | 9 | | `footerImage` | `footer-image` | The image to use next to the footer text. | `string` | `undefined` | 10 | | `timestamp` | `timestamp` | The timestamp to use for the message date. When supplying a string, the format must be `01/31/2000`. | `Date \| null \| string \| undefined` | `undefined` | 11 | 12 | --- 13 | 14 | _Built with [StencilJS](https://stenciljs.com/)_ 15 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/voice-channel.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function VoiceChannel(props: T) { 4 | return ( 5 | 6 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/static/eyes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reaction/readme.md: -------------------------------------------------------------------------------- 1 | # discord-reaction 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------- | ------------- | -------------------------------------------------------- | --------- | ----------- | 9 | | `count` | `count` | The number of people who reacted. | `number` | `1` | 10 | | `emoji` | `emoji` | The reaction emoji image URL. | `string` | `undefined` | 11 | | `interactive` | `interactive` | Whether the reaction should be reactive. | `boolean` | `false` | 12 | | `name` | `name` | The name of the emoji to use as alternative image text. | `string` | `':emoji:'` | 13 | | `reacted` | `reacted` | Whether the reaction should show as reacted by the user. | `boolean` | `false` | 14 | 15 | --- 16 | 17 | _Built with [StencilJS](https://stenciljs.com/)_ 18 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-code-block/discord-code-block.css: -------------------------------------------------------------------------------- 1 | /* .discord-message .discord-message-body .discord-spoiler */ 2 | 3 | .discord-code-block-pre { 4 | background-color: #2f3136; 5 | font-family: 'Consolas', 'Courier New', Courier, monospace; 6 | margin-top: 0.25rem; 7 | white-space: pre-wrap; 8 | } 9 | 10 | .discord-code-block-pre--multiline { 11 | display: block; 12 | margin-bottom: 0.5rem; 13 | margin-top: 0.25em; 14 | padding: 0.5em; 15 | border: 1px solid #202225; 16 | border-radius: 4px; 17 | color: #b9bbbe; 18 | font-size: 0.875rem; 19 | } 20 | 21 | .discord-code-block-pre--multiline.hljs { 22 | background-color: #2f3136; 23 | color: #b9bbbe; 24 | } 25 | 26 | .discord-embed .discord-code-block-pre { 27 | background-color: #202225; 28 | } 29 | 30 | .discord-embed .discord-code-block-pre .hljs { 31 | background-color: #202225; 32 | } 33 | 34 | code.hljs { 35 | padding: unset !important; 36 | } 37 | 38 | /* I dont know what theme discord uses, but i thought this was close enough */ 39 | @import '~highlight.js/styles/base16/helios.css'; 40 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-thread/discord-thread.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-thread', 5 | styleUrl: 'discord-thread.css' 6 | }) 7 | export class DiscordThread implements ComponentInterface { 8 | /** 9 | * The DiscordThread element. 10 | */ 11 | @Element() 12 | public el: HTMLElement; 13 | 14 | /** 15 | * The name of the thread. 16 | */ 17 | @Prop() 18 | public name = 'Thread'; 19 | 20 | /** 21 | * The the text within the call to action text. (i.e. 'See Thread' or 'x Messages') 22 | */ 23 | @Prop() 24 | public cta = 'See Thread'; 25 | 26 | public render() { 27 | return ( 28 | 29 |
30 | {this.name} 31 | 34 |
35 | 36 | 37 | 38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-tenor-video/discord-tenor-video.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-tenor-video', 5 | styleUrl: 'discord-tenor-video.css' 6 | }) 7 | export class DiscordTenorVideo implements ComponentInterface { 8 | /** 9 | * The DiscordTenorVideo element. 10 | */ 11 | @Element() 12 | public el: HTMLElement; 13 | 14 | /** 15 | * The URL for the video 16 | */ 17 | @Prop() 18 | public url: string; 19 | 20 | /** 21 | * The height of the video in pixels 22 | */ 23 | @Prop() 24 | public height: number; 25 | 26 | /** 27 | * The width of the video in pixels 28 | */ 29 | @Prop() 30 | public width: number; 31 | 32 | public render() { 33 | return ( 34 | 35 |
36 | 37 |
38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © `2020` `Aura Román, Jeroen Claassens` 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the “Software”), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/reply-icon.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function ReplyIcon(props: T) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/dm-call.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function DMCall(props: T) { 4 | return ( 5 | 6 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | Linting: 11 | name: Linting 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Project 15 | uses: actions/checkout@v3 16 | - name: Add problem matcher 17 | run: echo "::add-matcher::.github/problemMatchers/eslint.json" 18 | - name: Use Node.js v16 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16 22 | cache: yarn 23 | - name: Install Dependencies 24 | run: yarn --immutable 25 | - name: Run ESLint 26 | run: yarn lint --fix=false 27 | 28 | Building: 29 | name: Compile source code 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout Project 33 | uses: actions/checkout@v3 34 | - name: Add problem matcher 35 | run: echo "::add-matcher::.github/problemMatchers/tsc.json" 36 | - name: Use Node.js v16 37 | uses: actions/setup-node@v3 38 | with: 39 | node-version: 16 40 | cache: yarn 41 | - name: Install Dependencies 42 | run: yarn --immutable 43 | - name: Build Code 44 | run: yarn build 45 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/dm-missed-call.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function DMMissedCall(props: T) { 4 | return ( 5 | 6 | 7 | 8 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/channel-icon.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function ChannelIcon(props: T) { 4 | return ( 5 | 6 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-custom-emoji/readme.md: -------------------------------------------------------------------------------- 1 | # discord-custom-emoji 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | 9 | | `embedEmoji` | `embed-emoji` | Determines whether or not the emoji is used in an embed, or a message. If it is used in an embed, the sizing is adjusted accordingly. | `boolean` | `undefined` | 10 | | `largeEmoji` | `large-emoji` | The emoji size | `boolean` | `false` | 11 | | `name` | `name` | The name of the emoji | `string` | `undefined` | 12 | | `url` | `url` | The emoji URL to use in the message. | `string` | `undefined` | 13 | 14 | --- 15 | 16 | _Built with [StencilJS](https://stenciljs.com/)_ 17 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-thread/discord-thread.css: -------------------------------------------------------------------------------- 1 | .discord-thread { 2 | background-color: #2f3136; 3 | border-radius: 4px; 4 | cursor: pointer; 5 | margin-top: 8px; 6 | max-width: 480px; 7 | min-width: 0; 8 | padding: 8px; 9 | display: inline-flex; 10 | width: fit-content; 11 | flex-direction: column; 12 | } 13 | 14 | .discord-light-theme .discord-thread { 15 | background-color: #f2f3f5; 16 | } 17 | 18 | .discord-thread .discord-thread-top { 19 | display: flex; 20 | } 21 | 22 | .discord-thread .discord-thread-bottom { 23 | font-size: 0.875rem; 24 | line-height: 1.125rem; 25 | align-items: center; 26 | color: #b9bbbe; 27 | display: flex; 28 | margin-top: 2px; 29 | white-space: nowrap; 30 | } 31 | 32 | .discord-light-theme .discord-thread-bottom { 33 | color: #4f5660; 34 | } 35 | 36 | .discord-thread .discord-thread-name { 37 | font-size: 0.875rem; 38 | font-weight: 600; 39 | line-height: 1.125rem; 40 | color: white; 41 | margin-right: 8px; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | } 46 | 47 | .discord-light-theme .discord-thread-name { 48 | color: #060607; 49 | } 50 | 51 | .discord-thread .discord-thread-cta { 52 | color: #00aff4; 53 | flex-shrink: 0; 54 | font-size: 0.875rem; 55 | font-weight: 600; 56 | line-height: 1.125rem; 57 | } 58 | 59 | .discord-thread:hover .discord-thread-cta { 60 | text-decoration: underline; 61 | } 62 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/partner-badge-overlay.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | const PartnerBadgeOverlay = () => ( 4 | 5 | 9 | 13 | 14 | ); 15 | 16 | export default PartnerBadgeOverlay; 17 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-header/discord-header.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Host, h, Prop, ComponentInterface } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'discord-header', 5 | styleUrl: 'discord-header.css' 6 | }) 7 | export class DiscordHeader implements ComponentInterface { 8 | /** 9 | * The guild name 10 | */ 11 | @Prop() 12 | public guild: string; 13 | 14 | /** 15 | * The name of the channel 16 | */ 17 | @Prop() 18 | public channel: string; 19 | 20 | /** 21 | * The icon to display. 22 | */ 23 | @Prop() 24 | public icon?: string; 25 | 26 | public render() { 27 | return ( 28 | 29 |
30 | { 31 | // if no guild icon, create one using the first letter of the guild name 32 | this.icon ? ( 33 | guild icon 34 | ) : ( 35 |
36 | 37 | {(() => { 38 | const split = this.guild.split(' '); 39 | return split.length > 1 ? split[0][0] + split[1][0] : split[0][0]; 40 | })()} 41 | 42 |
43 | ) 44 | } 45 |
46 |
47 |
{this.guild}
48 |
#{this.channel}
49 | 50 |
51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-command/readme.md: -------------------------------------------------------------------------------- 1 | # discord-command 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ----------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | 9 | | `author` | `author` | The message author's username. | `string` | `'User'` | 10 | | `avatar` | `avatar` | The message author's avatar. Can be an avatar shortcut, relative path, or external link. | `string` | `undefined` | 11 | | `command` | `command` | The name of the command invoked. | `string` | `undefined` | 12 | | `profile` | `profile` | The id of the profile data to use. | `string` | `undefined` | 13 | | `roleColor` | `role-color` | The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | 14 | 15 | --- 16 | 17 | _Built with [StencilJS](https://stenciljs.com/)_ 18 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/thread.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function Thread(props: T) { 4 | return ( 5 | 6 | 10 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/channel-thread.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function ChannelThread(props: T) { 4 | return ( 5 | 6 | 10 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-mention/discord-mention.css: -------------------------------------------------------------------------------- 1 | .discord-message .discord-mention { 2 | color: #e3e7f8; 3 | background-color: hsla(235, 85.6%, 64.7%, 0.3); 4 | font-weight: 500; 5 | padding: 0 2px; 6 | border-radius: 3px; 7 | unicode-bidi: -moz-plaintext; 8 | unicode-bidi: plaintext; 9 | -webkit-transition: background-color 50ms ease-out, color 50ms ease-out; 10 | transition: background-color 50ms ease-out, color 50ms ease-out; 11 | cursor: pointer; 12 | } 13 | 14 | .discord-message .discord-mention:hover { 15 | color: #fff; 16 | background-color: hsl(235, 85.6%, 64.7%); 17 | } 18 | 19 | .discord-message .discord-mention.discord-channel-mention { 20 | padding-left: 1.2rem !important; 21 | position: relative; 22 | } 23 | 24 | .discord-message .discord-mention.discord-voice-mention, 25 | .discord-message .discord-mention.discord-locked-mention, 26 | .discord-message .discord-mention.discord-thread-mention, 27 | .discord-message .discord-mention.discord-forum-mention { 28 | padding-left: 1.25rem !important; 29 | position: relative; 30 | } 31 | 32 | .discord-light-theme .discord-message .discord-mention { 33 | color: #687dc6; 34 | background-color: hsla(235, 85.6%, 64.7%, 0.15); 35 | } 36 | 37 | .discord-light-theme .discord-message .discord-mention:hover { 38 | color: #ffffff; 39 | background-color: hsl(235, 85.6%, 64.7%); 40 | } 41 | 42 | .discord-message .discord-mention .discord-mention-icon { 43 | width: 1rem; 44 | height: 1rem; 45 | object-fit: contain; 46 | position: absolute; 47 | left: 0.125rem; 48 | top: 0.125rem; 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-attachment/readme.md: -------------------------------------------------------------------------------- 1 | # discord-attachment 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | -------- | --------- | --------------------------------------------------------------------------- | ----------------------------------------- | ---------------------- | 9 | | `alt` | `alt` | The alt text to show in case the image was unable to load | `string \| undefined` | `'discord attachment'` | 10 | | `height` | `height` | The height of the image in pixels | `number \| undefined` | `undefined` | 11 | | `size` | `size` | The size of the file. | `string` | `undefined` | 12 | | `type` | `type` | The type of file the attachment is. 'image' \| 'video' \| 'audio' \| 'file' | `"audio" \| "file" \| "image" \| "video"` | `undefined` | 13 | | `url` | `url` | The URL for the image attachment | `string` | `undefined` | 14 | | `width` | `width` | The width of the image in pixels | `number \| undefined` | `undefined` | 15 | 16 | --- 17 | 18 | _Built with [StencilJS](https://stenciljs.com/)_ 19 | -------------------------------------------------------------------------------- /packages/core/static/thumbsup.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-messages/discord-messages.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, h, Host, Prop } from '@stencil/core'; 2 | import clsx from 'clsx'; 3 | import { defaultBackground, defaultMode, defaultTheme } from '../../options'; 4 | 5 | @Component({ 6 | tag: 'discord-messages', 7 | styleUrl: 'discord-messages.css' 8 | }) 9 | export class DiscordMessages implements ComponentInterface { 10 | /** 11 | * Whether to use light theme or not. 12 | */ 13 | @Prop({ mutable: true, reflect: true }) 14 | public lightTheme: boolean; 15 | 16 | /** 17 | * Whether to exclude the background or not. 18 | */ 19 | @Prop({ mutable: true, reflect: true }) 20 | public noBackground: boolean; 21 | 22 | /** 23 | * Whether to use compact mode or not. 24 | */ 25 | @Prop({ mutable: true, reflect: true }) 26 | public compactMode: boolean; 27 | 28 | public componentWillRender() { 29 | if (this.lightTheme || (defaultTheme === 'light' && this.lightTheme)) { 30 | this.lightTheme = true; 31 | } 32 | 33 | if (this.compactMode || (defaultMode === 'compact' && this.compactMode)) { 34 | this.compactMode = true; 35 | } 36 | 37 | if (this.noBackground || (defaultBackground === 'none' && this.noBackground)) { 38 | this.noBackground = true; 39 | } 40 | } 41 | 42 | public render() { 43 | return ( 44 | 54 | 55 | 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/core/src/components/svgs/channel-forum.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core'; 2 | 3 | export default function ChannelForum(props: T) { 4 | return ( 5 | 6 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-messages/discord-messages.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.bunny.net/css?family=roboto:400,500,700'); 2 | 3 | /* New Whitney fonts to match rebrading */ 4 | @font-face { 5 | font-family: 'Whitney'; 6 | src: url('https://cdn.jsdelivr.net/gh/ItzDerock/discord-components@master/assets/fonts/Book.woff') format('woff'); 7 | font-weight: 400; 8 | font-display: swap; 9 | } 10 | @font-face { 11 | font-family: 'Whitney'; 12 | src: url('https://cdn.jsdelivr.net/gh/ItzDerock/discord-components@master/assets/fonts/Medium.woff') format('woff'); 13 | font-weight: 500; 14 | font-display: swap; 15 | } 16 | @font-face { 17 | font-family: 'Whitney'; 18 | src: url('https://cdn.jsdelivr.net/gh/ItzDerock/discord-components@master/assets/fonts/Semibold.woff') format('woff'); 19 | font-weight: 600; 20 | font-display: swap; 21 | } 22 | @font-face { 23 | font-family: 'Whitney'; 24 | src: url('https://cdn.jsdelivr.net/gh/ItzDerock/discord-components@master/assets/fonts/Bold.woff') format('woff'); 25 | font-weight: 700; 26 | font-display: swap; 27 | } 28 | 29 | .discord-messages { 30 | color: #fff; 31 | background-color: #36393e; 32 | display: block; 33 | font-size: 16px; 34 | font-family: Whitney, 'Source Sans Pro', ui-sans-serif, system-ui, -apple-system, 'system-ui', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 35 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 36 | line-height: 170%; 37 | border: 1px solid rgba(255, 255, 255, 0.05); 38 | } 39 | 40 | .discord-messages.discord-light-theme { 41 | color: #747f8d; 42 | background-color: #fff; 43 | border-color: #dedede; 44 | } 45 | 46 | .discord-messages.discord-no-background { 47 | background-color: unset; 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-mention/readme.md: -------------------------------------------------------------------------------- 1 | # discord-mention 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ----------- | 9 | | `color` | `color` | The color to use for this mention. Only works for role mentions and must be in hex format. | `string` | `undefined` | 10 | | `highlight` | `highlight` | Whether this entire message block should be highlighted (to emulate the "logged in user" being pinged). | `boolean` | `false` | 11 | | `type` | `type` | The type of mention this should be. This will prepend the proper prefix character. Valid values: `user`, `channel`, `role`, `voice`, `locked`, `thread`, `forum`, and `slash`. | `"channel" \| "forum" \| "locked" \| "role" \| "slash" \| "thread" \| "user" \| "voice"` | `'user'` | 12 | 13 | --- 14 | 15 | _Built with [StencilJS](https://stenciljs.com/)_ 16 | -------------------------------------------------------------------------------- /packages/react/src/react-component-lib/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type { StyleReactProps } from '../interfaces'; 4 | 5 | export type StencilReactExternalProps = PropType & Omit, 'style'> & StyleReactProps; 6 | 7 | // This will be replaced with React.ForwardedRef when react-output-target is upgraded to React v17 8 | export type StencilReactForwardedRef = ((instance: T | null) => void) | React.MutableRefObject | null; 9 | 10 | export const setRef = (ref: StencilReactForwardedRef | React.Ref | undefined, value: any) => { 11 | if (typeof ref === 'function') { 12 | ref(value); 13 | } else if (ref != null) { 14 | // Cast as a MutableRef so we can assign current 15 | (ref as React.MutableRefObject).current = value; 16 | } 17 | }; 18 | 19 | export const mergeRefs = (...refs: (StencilReactForwardedRef | React.Ref | undefined)[]): React.RefCallback => { 20 | return (value: any) => { 21 | refs.forEach((ref) => { 22 | setRef(ref, value); 23 | }); 24 | }; 25 | }; 26 | 27 | export const createForwardRef = (ReactComponent: any, displayName: string) => { 28 | const forwardRef = (props: StencilReactExternalProps, ref: StencilReactForwardedRef) => { 29 | return ; 30 | }; 31 | forwardRef.displayName = displayName; 32 | 33 | return React.forwardRef(forwardRef); 34 | }; 35 | 36 | export const defineCustomElement = (tagName: string, customElement: any) => { 37 | if (customElement !== undefined && typeof customElements !== 'undefined' && !customElements.get(tagName)) { 38 | customElements.define(tagName, customElement); 39 | } 40 | }; 41 | 42 | export * from './attachProps'; 43 | export * from './case'; 44 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-description/discord-embed-description.css: -------------------------------------------------------------------------------- 1 | .discord-embed .discord-embed-description { 2 | font-size: 0.875rem; 3 | font-weight: 400; 4 | grid-column: 1/1; 5 | line-height: 1.125rem; 6 | margin-top: 8px; 7 | min-width: 0; 8 | white-space: pre-line; 9 | } 10 | 11 | .discord-embed .discord-embed-description pre { 12 | margin: 0; 13 | margin-top: 6px; 14 | } 15 | 16 | .discord-embed .discord-embed-description img.emoji { 17 | width: 22px; 18 | height: 22px; 19 | } 20 | 21 | .discord-embed .discord-embed-description blockquote { 22 | position: relative; 23 | padding: 0 8px 0 12px; 24 | margin: 0; 25 | } 26 | 27 | .discord-embed .discord-embed-description blockquote::before { 28 | content: ''; 29 | display: block; 30 | position: absolute; 31 | left: 0; 32 | height: 100%; 33 | width: 4px; 34 | border-radius: 4px; 35 | background-color: #4f545c; 36 | } 37 | 38 | .discord-light-theme .discord-embed-description blockquote::before { 39 | background-color: #c7ccd1; 40 | } 41 | 42 | .discord-embed .discord-embed-description .spoiler { 43 | background-color: #202225; 44 | color: transparent; 45 | cursor: pointer; 46 | } 47 | 48 | .discord-light-theme .discord-embed .discord-embed-description .spoiler { 49 | background-color: #b9bbbe; 50 | } 51 | 52 | .discord-embed .discord-embed-description .spoiler:hover { 53 | background-color: rgba(32, 34, 37, 0.8); 54 | } 55 | 56 | .discord-light-theme .discord-embed .discord-embed-description .spoiler:hover { 57 | background-color: rgba(185, 187, 190, 0.8); 58 | } 59 | 60 | .discord-embed .discord-embed-description .spoiler:active { 61 | color: inherit; 62 | background-color: hsla(0, 0%, 100%, 0.1); 63 | } 64 | 65 | .discord-light-theme .discord-embed .discord-embed-description .spoiler:active { 66 | background-color: rgba(0, 0, 0, 0.1); 67 | } 68 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@derockdev/discord-components-react", 3 | "version": "3.6.1", 4 | "description": "React bindings for @derockdev/discord-components-core", 5 | "author": "@derockdev", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "module": "dist/index.mjs", 9 | "typings": "dist/index.d.ts", 10 | "exports": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.js" 14 | }, 15 | "sideEffects": [ 16 | "./dist/index.mjs", 17 | "./dist/index.js" 18 | ], 19 | "homepage": "https://github.com/itzderock/discord-components/tree/main/packages/react#readme", 20 | "scripts": { 21 | "clean": "node scripts/clean.mjs", 22 | "build": "yarn clean && tsc && gen-esm-wrapper dist/index.js dist/index.mjs && replace-in-file --configFile=scripts/replaceDefaultMod.cjs" 23 | }, 24 | "dependencies": { 25 | "@derockdev/discord-components-core": "^3.6.1", 26 | "tslib": "^2.6.0" 27 | }, 28 | "peerDependencies": { 29 | "react": "16.8.x || 17.x || 18.x", 30 | "react-dom": "16.8.x || 17.x || 18.x" 31 | }, 32 | "directories": { 33 | "lib": "src" 34 | }, 35 | "files": [ 36 | "dist", 37 | "!dist/*.tsbuildinfo" 38 | ], 39 | "engines": { 40 | "node": ">=v14.0.0" 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/skyra-project/discord-components.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/skyra-project/discord-components/issues" 51 | }, 52 | "keywords": [ 53 | "skyra", 54 | "typescript", 55 | "ts", 56 | "yarn", 57 | "discord", 58 | "bot", 59 | "components", 60 | "webcomponents", 61 | "stencil", 62 | "react" 63 | ], 64 | "devDependencies": { 65 | "gen-esm-wrapper": "^1.1.3", 66 | "replace-in-file": "^6.3.5" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reaction/discord-reaction.css: -------------------------------------------------------------------------------- 1 | .discord-reaction { 2 | border-radius: 0.5rem; 3 | cursor: pointer; 4 | flex-shrink: 0; 5 | margin-right: 0.25rem; 6 | margin-bottom: 0.25rem; 7 | user-select: none; 8 | transition: none 0.1s ease; 9 | transition-property: background-color, border-color; 10 | background-color: #2f3136; 11 | border: 1px solid transparent; 12 | } 13 | 14 | .discord-light-theme .discord-reaction { 15 | background-color: #f2f3f5; 16 | } 17 | 18 | .discord-reaction:hover { 19 | background-color: #36393f; 20 | border-color: #fff2; 21 | } 22 | 23 | .discord-light-theme .discord-reaction:not(.discord-reaction-reacted):hover { 24 | background-color: white; 25 | border-color: #0003; 26 | } 27 | 28 | .discord-reaction.discord-reaction-reacted { 29 | background-color: rgba(88, 101, 242, 0.15); 30 | border-color: #5865f2; 31 | } 32 | 33 | .discord-light-theme .discord-reaction.discord-reaction-reacted { 34 | background-color: #e7e9fd; 35 | } 36 | 37 | .discord-reaction .discord-reaction-inner { 38 | display: flex; 39 | align-items: center; 40 | padding: 0.125rem 0.375rem; 41 | } 42 | 43 | .discord-reaction img { 44 | width: 1rem; 45 | height: 1rem; 46 | margin: 0.125rem 0; 47 | min-width: auto; 48 | min-height: auto; 49 | object-fit: contain; 50 | vertical-align: bottom; 51 | } 52 | 53 | .discord-reaction .discord-reaction-count { 54 | font-size: 0.875rem; 55 | font-weight: 500; 56 | margin-left: 0.375rem; 57 | text-align: center; 58 | color: #b9bbbe; 59 | } 60 | 61 | .discord-light-theme .discord-reaction .discord-reaction-count { 62 | color: #4f5660; 63 | } 64 | 65 | .discord-reaction.discord-reaction-reacted .discord-reaction-count { 66 | color: #dee0fc; 67 | } 68 | 69 | .discord-light-theme .discord-reaction.discord-reaction-reacted .discord-reaction-count { 70 | color: #5865f2; 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-button/discord-button.css: -------------------------------------------------------------------------------- 1 | .discord-button { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | cursor: pointer; 6 | margin: 4px 8px 4px 0; 7 | padding: 2px 16px; 8 | width: auto; 9 | height: 32px; 10 | min-width: 60px; 11 | min-height: 32px; 12 | -webkit-transition: background-color 0.17s ease, color 0.17s ease; 13 | transition: background-color 0.17s ease, color 0.17s ease; 14 | border-radius: 3px; 15 | font-size: 14px; 16 | font-weight: 500; 17 | line-height: 16px; 18 | text-decoration: none !important; 19 | } 20 | 21 | .discord-button.discord-button-success { 22 | color: #fff; 23 | background-color: #3ba55d; 24 | } 25 | 26 | .discord-button.discord-button-success.discord-button-hoverable:hover { 27 | background-color: #2d7d46; 28 | } 29 | 30 | .discord-button.discord-button-destructive { 31 | color: #fff; 32 | background-color: #ed4245; 33 | } 34 | 35 | .discord-button.discord-button-destructive.discord-button-hoverable:hover { 36 | background-color: #c03537; 37 | } 38 | 39 | .discord-button.discord-button-primary { 40 | color: #fff; 41 | background-color: #5865f2; 42 | } 43 | 44 | .discord-button.discord-button-primary.discord-button-hoverable:hover { 45 | background-color: #4752c4; 46 | } 47 | 48 | .discord-button.discord-button-secondary { 49 | color: #fff; 50 | background-color: #4f545c; 51 | } 52 | 53 | .discord-button.discord-button-secondary.discord-button-hoverable:hover { 54 | background-color: #5d6269; 55 | } 56 | 57 | .discord-button.discord-button-disabled { 58 | cursor: not-allowed; 59 | opacity: 0.5; 60 | } 61 | 62 | .discord-button .discord-button-launch { 63 | margin-left: 8px; 64 | } 65 | 66 | .discord-button .discord-button-emoji { 67 | margin-right: 4px; 68 | object-fit: contain; 69 | width: 1.375em; 70 | height: 1.375em; 71 | vertical-align: bottom; 72 | } 73 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-button/readme.md: -------------------------------------------------------------------------------- 1 | # discord-button 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ----------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------- | 9 | | `disabled` | `disabled` | Whether to show the button as disabled. | `boolean` | `false` | 10 | | `emoji` | `emoji` | The emoji URL to use in the button. | `string` | `undefined` | 11 | | `emojiName` | `emoji-name` | The name of the emoji used in the button. | `string` | `'emoji'` | 12 | | `type` | `type` | The type of button this is, this will change the color of the button. Valid values: `primary`, `secondary`, `success`, `destructive`. | `"destructive" \| "primary" \| "secondary" \| "success"` | `'secondary'` | 13 | | `url` | `url` | The URL for the button. Setting this will force the button type to be `secondary`. | `string` | `undefined` | 14 | 15 | --- 16 | 17 | _Built with [StencilJS](https://stenciljs.com/)_ 18 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-system-message/readme.md: -------------------------------------------------------------------------------- 1 | # discord-system-message 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------ | 9 | | `channelName` | `channel-name` | Whether this message is to show channel name changes, used to match Discord's style. | `boolean` | `false` | 10 | | `timestamp` | `timestamp` | The timestamp to use for the message date. | `Date \| null \| string` | `new Date()` | 11 | | `type` | `type` | The type of system message this is, this will change the icon shown. Valid values: `join`, `leave`, `call`, `missed-call`, `boost`, `edit`, `thread`, `pin`, `alert`, and `error`. | `"alert" \| "boost" \| "call" \| "edit" \| "error" \| "join" \| "leave" \| "missed-call" \| "pin" \| "thread"` | `'join'` | 12 | 13 | --- 14 | 15 | _Built with [StencilJS](https://stenciljs.com/)_ 16 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-custom-emoji/discord-custom-emoji.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Prop } from '@stencil/core'; 2 | import { getGlobalEmojiUrl } from '../../util'; 3 | 4 | @Component({ 5 | tag: 'discord-custom-emoji', 6 | styleUrl: 'discord-custom-emoji.css' 7 | }) 8 | export class DiscordCustomEmoji implements ComponentInterface { 9 | /** 10 | * The DiscordReaction element. 11 | */ 12 | @Element() 13 | public el: HTMLElement; 14 | 15 | /** 16 | * The name of the emoji 17 | */ 18 | @Prop() 19 | public name: string; 20 | 21 | /** 22 | * The emoji URL to use in the message. 23 | */ 24 | @Prop({ mutable: true }) 25 | public url: string; 26 | 27 | /** 28 | * Determines whether or not the emoji is used in an embed, or a message. 29 | * If it is used in an embed, the sizing is adjusted accordingly. 30 | */ 31 | @Prop({ mutable: true }) 32 | public embedEmoji: boolean; 33 | 34 | /** 35 | * The emoji size 36 | */ 37 | @Prop() 38 | public largeEmoji = false; 39 | 40 | public componentWillRender() { 41 | if (!this.url && Boolean(this.name)) { 42 | const emojiFromGlobal = getGlobalEmojiUrl(this.name); 43 | 44 | if (emojiFromGlobal) { 45 | this.url ??= emojiFromGlobal.url ?? ''; 46 | this.embedEmoji ??= emojiFromGlobal.embedEmoji ?? false; 47 | } 48 | } 49 | } 50 | 51 | public render() { 52 | const name = `:${this.name}:`; 53 | const emojiClassName = this.embedEmoji ? 'discord-embed-custom-emoji' : 'discord-custom-emoji'; 54 | let emojiImageClassName = this.embedEmoji ? 'discord-embed-custom-emoji-image' : 'discord-custom-emoji-image'; 55 | 56 | if (this.largeEmoji) { 57 | emojiImageClassName += ' discord-custom-emoji-image-large'; 58 | } 59 | 60 | return ( 61 | 62 | {name} 63 | 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-footer/discord-embed-footer.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop, Watch } from '@stencil/core'; 2 | import Fragment from '../../Fragment'; 3 | import { DiscordTimestamp, handleTimestamp } from '../../util'; 4 | 5 | @Component({ 6 | tag: 'discord-embed-footer', 7 | styleUrl: 'discord-embed-footer.css' 8 | }) 9 | export class DiscordEmbedFooter implements ComponentInterface { 10 | /** 11 | * The DiscordEmbedFooter element. 12 | */ 13 | @Element() 14 | public el: HTMLElement; 15 | 16 | /** 17 | * The image to use next to the footer text. 18 | */ 19 | @Prop() 20 | public footerImage: string; 21 | 22 | /** 23 | * The timestamp to use for the message date. When supplying a string, the format must be `01/31/2000`. 24 | */ 25 | @Prop({ mutable: true, reflect: true }) 26 | public timestamp?: DiscordTimestamp; 27 | 28 | @Watch('timestamp') 29 | public updateTimestamp(value?: DiscordTimestamp): string | null { 30 | if (!value || isNaN(new Date(value).getTime())) return null; 31 | return handleTimestamp(new Date(value)); 32 | } 33 | 34 | public componentWillRender() { 35 | this.timestamp = this.updateTimestamp(this.timestamp); 36 | } 37 | 38 | public render() { 39 | const parent: HTMLDiscordMessagesElement = this.el.parentElement as HTMLDiscordMessagesElement; 40 | 41 | if (parent.tagName.toLowerCase() !== 'div') { 42 | throw new Error('All components must be direct children of .'); 43 | } 44 | 45 | return ( 46 | 47 | {this.footerImage ? : ''} 48 | 49 | 50 | {this.timestamp ? : ''} 51 | {this.timestamp ? {this.timestamp} : ''} 52 | 53 | 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-attachment/discord-attachment.css: -------------------------------------------------------------------------------- 1 | .discord-attachment { 2 | color: #dcddde; 3 | display: flex; 4 | font-size: 13px; 5 | line-height: 150%; 6 | margin-bottom: 8px; 7 | margin-top: 8px; 8 | } 9 | 10 | .discord-attachment .discord-image-wrapper { 11 | display: block; 12 | position: relative; 13 | -webkit-user-select: text; 14 | -moz-user-select: text; 15 | -ms-user-select: text; 16 | user-select: text; 17 | overflow: hidden; 18 | border-radius: 3px; 19 | } 20 | 21 | .discord-attachment .discord-image-wrapper img, 22 | .discord-attachment .discord-image-wrapper video { 23 | max-width: 400px; 24 | width: 100%; 25 | height: auto; 26 | } 27 | 28 | .discord-attachment-generic { 29 | display: flex; 30 | width: auto; 31 | max-width: 520px; 32 | height: 40px; 33 | padding: 10px; 34 | background-color: #2f3136; 35 | border: 1px solid #292b2f; 36 | border-radius: 3px; 37 | overflow: hidden; 38 | } 39 | 40 | .discord-attachment-generic-icon { 41 | float: left; 42 | height: 100%; 43 | width: auto; 44 | } 45 | 46 | .discord-attachment-generic-icon > svg { 47 | width: 30px; 48 | height: 100%; 49 | margin-right: 10px; 50 | } 51 | 52 | .discord-attachment-generic-inner { 53 | flex-grow: 1; 54 | width: fit-content; 55 | height: 100%; 56 | } 57 | 58 | .discord-attachment-generic-size { 59 | color: #72767d; 60 | font-size: 12px; 61 | align-items: flex-start; 62 | line-height: 100%; 63 | } 64 | 65 | .discord-attachment-generic-name { 66 | overflow: hidden; 67 | white-space: nowrap; 68 | text-overflow: ellipsis; 69 | width: fit-content; 70 | } 71 | 72 | .discord-attachment-generic-download { 73 | float: right; 74 | height: 100%; 75 | margin-left: 5px; 76 | margin-right: 5px; 77 | } 78 | 79 | .discord-attachment-generic-download > a > svg { 80 | height: 100%; 81 | color: rgb(185, 187, 190); 82 | } 83 | 84 | .discord-attachment-generic-download > a > svg:hover { 85 | color: rgb(220, 221, 222); 86 | } 87 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reaction/discord-reaction.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Prop } from '@stencil/core'; 2 | import clsx from 'clsx'; 3 | 4 | @Component({ 5 | tag: 'discord-reaction', 6 | styleUrl: 'discord-reaction.css' 7 | }) 8 | export class DiscordReaction implements ComponentInterface { 9 | /** 10 | * The DiscordReaction element. 11 | */ 12 | @Element() 13 | public el: HTMLElement; 14 | 15 | /** 16 | * The reaction emoji image URL. 17 | */ 18 | @Prop() 19 | public emoji: string; 20 | 21 | /** 22 | * The name of the emoji to use as alternative image text. 23 | * @default ':emoji' 24 | */ 25 | @Prop() 26 | public name = ':emoji:'; 27 | 28 | /** 29 | * The number of people who reacted. 30 | * @default 1 31 | */ 32 | @Prop({ mutable: true }) 33 | public count = 1; 34 | 35 | /** 36 | * Whether the reaction should show as reacted by the user. 37 | * @default false 38 | */ 39 | @Prop() 40 | public reacted = false; 41 | 42 | /** 43 | * Whether the reaction should be reactive. 44 | * @remark When the reaction is interactive left clicking it will add 1 to the counter. 45 | * Whereas when holding the Shift key and left clicking it will decrease the counter. 46 | * The counter cannot go below 1. 47 | * @default false 48 | */ 49 | @Prop() 50 | public interactive = false; 51 | 52 | public render() { 53 | return ( 54 |
55 |
56 | {this.name} 57 | {this.count} 58 |
59 |
60 | ); 61 | } 62 | 63 | private handleReactionClick(event: MouseEvent) { 64 | if (this.interactive) { 65 | if (event.shiftKey) { 66 | this.count--; 67 | } else { 68 | this.count++; 69 | } 70 | 71 | if (this.count <= 0) { 72 | this.count = 1; 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-thread-message/discord-thread-message.css: -------------------------------------------------------------------------------- 1 | .discord-thread-message { 2 | height: 18px; 3 | min-width: 0; 4 | display: flex; 5 | align-items: center; 6 | font-size: 0.875rem; 7 | line-height: 1.125rem; 8 | } 9 | 10 | .discord-thread-message .discord-thread-message-avatar { 11 | margin-right: 8px; 12 | flex: 0 0 auto; 13 | width: 16px; 14 | height: 16px; 15 | border-radius: 50%; 16 | user-select: none; 17 | } 18 | 19 | .discord-thread-message .discord-thread-message-username { 20 | flex-shrink: 0; 21 | font-size: inherit; 22 | line-height: inherit; 23 | margin-right: 0.25rem; 24 | opacity: 0.64; 25 | color: white; 26 | display: inline; 27 | vertical-align: baseline; 28 | position: relative; 29 | overflow: hidden; 30 | } 31 | 32 | .discord-light-theme .discord-thread-message .discord-thread-message-username { 33 | color: #060607; 34 | } 35 | 36 | .discord-thread-message .discord-application-tag { 37 | background-color: #5865f2; 38 | color: #fff; 39 | font-size: 0.65em; 40 | margin-right: 5px; 41 | border-radius: 3px; 42 | line-height: 100%; 43 | text-transform: uppercase; 44 | display: flex; 45 | align-items: center; 46 | height: 0.9375rem; 47 | padding: 0 0.275rem; 48 | margin-top: 0.075em; 49 | border-radius: 0.1875rem; 50 | } 51 | 52 | .discord-thread-message .discord-application-tag-verified { 53 | display: inline-block; 54 | width: 0.9375rem; 55 | height: 0.9375rem; 56 | margin-left: -0.25rem; 57 | } 58 | 59 | .discord-thread-message .discord-thread-message-content { 60 | display: flex; 61 | align-items: baseline; 62 | } 63 | 64 | .discord-thread-message .discord-message-edited { 65 | color: #72767d; 66 | font-size: 10px; 67 | margin-left: 5px; 68 | } 69 | 70 | .discord-thread-message .discord-thread-message-timestamp { 71 | color: #72767d; 72 | flex-shrink: 0; 73 | margin-left: 8px; 74 | font-size: 0.875rem; 75 | line-height: 1.125rem; 76 | } 77 | 78 | .discord-light-theme .discord-thread-message .discord-thread-message-timestamp, 79 | .discord-light-theme .discord-thread-message .discord-message-edited { 80 | color: #747f8d; 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/src/options.ts: -------------------------------------------------------------------------------- 1 | export interface Avatars { 2 | default: 'blue' | 'gray' | 'green' | 'orange' | 'red'; 3 | blue?: string; 4 | gray?: string; 5 | green?: string; 6 | orange?: string; 7 | red?: string; 8 | [key: string]: string | undefined; 9 | } 10 | 11 | export interface Profile { 12 | author?: string; 13 | avatar?: string; 14 | bot?: boolean; 15 | verified?: boolean; 16 | server?: boolean; 17 | op?: boolean; 18 | roleColor?: string; 19 | roleIcon?: string; 20 | roleName?: string; 21 | } 22 | 23 | export interface DiscordMessageOptions { 24 | avatars?: Avatars; 25 | profiles?: { [key: string]: Profile }; 26 | emojis?: { [key: string]: Emoji }; 27 | defaultTheme?: string; 28 | defaultMode?: string; 29 | defaultBackground?: 'discord' | 'none'; 30 | } 31 | 32 | export const defaultDiscordAvatars: Omit = { 33 | blue: 'https://cdn.discordapp.com/embed/avatars/0.png', 34 | gray: 'https://cdn.discordapp.com/embed/avatars/1.png', 35 | green: 'https://cdn.discordapp.com/embed/avatars/2.png', 36 | orange: 'https://cdn.discordapp.com/embed/avatars/3.png', 37 | red: 'https://cdn.discordapp.com/embed/avatars/4.png', 38 | pink: 'https://cdn.discordapp.com/embed/avatars/5.png' 39 | }; 40 | 41 | export interface Emoji { 42 | name?: string; 43 | url?: string; 44 | embedEmoji?: boolean; 45 | } 46 | 47 | const globalAvatars: Avatars = window.$discordMessage?.avatars ?? ({} as Avatars); 48 | 49 | export const avatars: Avatars = Object.assign(defaultDiscordAvatars, globalAvatars, { 50 | default: defaultDiscordAvatars[globalAvatars.default] ?? globalAvatars.default ?? defaultDiscordAvatars.blue 51 | }); 52 | 53 | export const profiles: { [key: string]: Profile } = window.$discordMessage?.profiles ?? {}; 54 | 55 | export const defaultTheme: string = window.$discordMessage?.defaultTheme === 'light' ? 'light' : 'dark'; 56 | 57 | export const defaultMode: string = window.$discordMessage?.defaultMode === 'compact' ? 'compact' : 'cozy'; 58 | 59 | export const defaultBackground: string = window.$discordMessage?.defaultBackground === 'none' ? 'none' : 'discord'; 60 | 61 | declare global { 62 | interface Window { 63 | $discordMessage: DiscordMessageOptions; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/components/author-info/author-info.css: -------------------------------------------------------------------------------- 1 | .discord-message .discord-author-info { 2 | display: inline-flex; 3 | align-items: center; 4 | font-size: 16px; 5 | margin-right: 0.25rem; 6 | } 7 | 8 | .discord-compact-mode .discord-message .discord-author-info { 9 | margin-right: 0; 10 | } 11 | 12 | .discord-message .discord-author-info .discord-author-username { 13 | color: #fff; 14 | font-size: 1em; 15 | font-weight: 500; 16 | } 17 | 18 | .discord-light-theme .discord-message .discord-author-info .discord-author-username { 19 | color: #23262a; 20 | } 21 | 22 | .discord-message .discord-author-info .discord-application-tag { 23 | background-color: #5865f2; 24 | color: #fff; 25 | font-size: 0.625em; 26 | margin-left: 4px; 27 | border-radius: 3px; 28 | line-height: 100%; 29 | text-transform: uppercase; 30 | /* Use flex layout to ensure both verified icon and "BOT" text are aligned to center */ 31 | display: flex; 32 | align-items: center; 33 | /* Styling taken through Inspect Element on Discord client for Windows */ 34 | height: 0.9375rem; 35 | padding: 0 0.275rem; 36 | margin-top: 0.075em; 37 | border-radius: 0.1875rem; 38 | } 39 | 40 | .discord-message .discord-author-info .discord-application-tag.discord-application-tag-op { 41 | background-color: #c9cdfb; 42 | color: #4752c4; 43 | border-radius: 0.4rem; 44 | } 45 | 46 | .discord-message .discord-author-info .discord-application-tag-verified { 47 | display: inline-block; 48 | width: 0.9375rem; 49 | height: 0.9375rem; 50 | margin-left: -0.25rem; 51 | } 52 | 53 | .discord-message .discord-author-info .discord-author-role-icon { 54 | margin-left: 0.25rem; 55 | vertical-align: top; 56 | height: calc(1rem + 4px); 57 | width: calc(1rem + 4px); 58 | } 59 | 60 | .discord-compact-mode .discord-message .discord-author-info .discord-author-username { 61 | margin-left: 8px; 62 | margin-right: 4px; 63 | } 64 | 65 | .discord-compact-mode .discord-message .discord-author-info .discord-application-tag { 66 | margin-left: 0; 67 | margin-left: 5px; 68 | margin-right: 5px; 69 | padding-left: 10px; 70 | padding-right: 4px; 71 | } 72 | 73 | .discord-compact-mode .discord-message .discord-author-info .discord-application-tag-verified { 74 | margin-right: 0.7em; 75 | margin-left: -0.7em; 76 | } 77 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-invite/readme.md: -------------------------------------------------------------------------------- 1 | # discord-invite 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------- | -------------- | ------------------------------------------------------------------------------------ | --------------------- | ---------------------------------------- | 9 | | `icon` | `icon` | The server icon to display for the invite. | `string \| undefined` | `defaultDiscordAvatars.blue` | 10 | | `inviteTitle` | `invite-title` | Invitation embed title. | `string` | `"You've been invited to join a server"` | 11 | | `joinBtn` | `join-btn` | The join button. | `string` | `'Join'` | 12 | | `members` | `members` | The number of members on the server. | `number` | `0` | 13 | | `name` | `name` | The server's name. | `string` | `'Discord Server'` | 14 | | `online` | `online` | The number of members online on the server. | `number` | `0` | 15 | | `partnered` | `partnered` | Whether the server is partnered. Only works if `verified` is `false` or `undefined`. | `boolean` | `false` | 16 | | `url` | `url` | The URL to open when you click on the join button. | `string` | `undefined` | 17 | | `verified` | `verified` | Whether the server is verified. Only works if `partnered` is `false` or `undefined`. | `boolean` | `false` | 18 | 19 | --- 20 | 21 | _Built with [StencilJS](https://stenciljs.com/)_ 22 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-thread-message/readme.md: -------------------------------------------------------------------------------- 1 | # discord-thread-message 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | 9 | | `author` | `author` | The message author's username. | `string` | `'User'` | 10 | | `avatar` | `avatar` | The message author's avatar. Can be an avatar shortcut, relative path, or external link. | `string` | `undefined` | 11 | | `bot` | `bot` | Whether the message author is a bot or not. Only works if `server` is `false` or `undefined`. | `boolean` | `false` | 12 | | `edited` | `edited` | Whether the message has been edited or not. | `boolean` | `false` | 13 | | `profile` | `profile` | The id of the profile data to use. | `string` | `undefined` | 14 | | `relativeTimestamp` | `relative-timestamp` | The relative timestamp of the message. | `string` | `'1m ago'` | 15 | | `roleColor` | `role-color` | The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | 16 | | `server` | `server` | Whether the message author is a server crosspost webhook or not. Only works if `bot` is `false` or `undefined`. | `boolean` | `false` | 17 | | `verified` | `verified` | Whether the bot is verified or not. Only works if `bot` is `true` | `boolean` | `false` | 18 | 19 | --- 20 | 21 | _Built with [StencilJS](https://stenciljs.com/)_ 22 | -------------------------------------------------------------------------------- /packages/core/src/components/author-info/author-info.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent, h } from '@stencil/core'; 2 | import Fragment from '../../Fragment'; 3 | import VerifiedTick from '../svgs/verified-tick'; 4 | 5 | interface AuthorInfoProps { 6 | /** 7 | * The name of the author 8 | */ 9 | author: string; 10 | /** 11 | * Whether this author is a bot. Only works if `server` is `false` or `undefined`. 12 | */ 13 | bot: boolean; 14 | /** 15 | * Whether this author is a `server` crosspost webhook. Only works if `bot` is `false` or `undefined`. 16 | */ 17 | server: boolean; 18 | /** 19 | * Whether this author is the original poster. 20 | */ 21 | op: boolean; 22 | /** 23 | * The colour of the author, which comes from their highest role 24 | */ 25 | roleColor: string; 26 | /** 27 | * The role icon of the author, which comes from their highest role 28 | */ 29 | roleIcon: string; 30 | /** 31 | * The role name of the author, which comes from their highest role 32 | */ 33 | roleName: string; 34 | /** 35 | * Whether this bot is verified by Discord. Only works if `bot` is `true` 36 | */ 37 | verified: boolean; 38 | /** 39 | * Whether to reverse the order of the author info for compact mode. 40 | */ 41 | compact: boolean; 42 | } 43 | 44 | export const AuthorInfo: FunctionalComponent = ({ author, bot, server, op, roleColor, roleIcon, roleName, verified, compact }) => ( 45 | 46 | {!compact && ( 47 | 48 | 49 | {author} 50 | 51 | {roleIcon && {roleName}} 52 | 53 | )} 54 | { 55 | 56 | {/* If bot is true then we need to render a Bot tag */} 57 | {bot && !server && ( 58 | 59 | {/* If verified is true then a verified checkmark should be prefixed */} 60 | {verified && } 61 | Bot 62 | 63 | )} 64 | {server && !bot && Server} 65 | {op && OP} 66 | 67 | } 68 | {compact && ( 69 | 70 | {author} 71 | 72 | )} 73 | 74 | ); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "private": true, 7 | "scripts": { 8 | "clean": "node scripts/clean.mjs", 9 | "lint": "eslint packages/ --fix --ext ts,tsx", 10 | "format": "prettier --write --loglevel=warn \"packages/**/src/**\"", 11 | "start": "yarn workspace @derockdev/discord-components-core start", 12 | "build": "yarn clean && yarn build:core && yarn build:react && yarn lint && yarn format", 13 | "build:core": "yarn workspace @derockdev/discord-components-core build", 14 | "build:react": "yarn workspace @derockdev/discord-components-react build", 15 | "postinstall": "husky install .github/husky" 16 | }, 17 | "devDependencies": { 18 | "@commitlint/cli": "^17.6.6", 19 | "@commitlint/config-conventional": "^17.6.6", 20 | "@lerna-lite/cli": "^2.5.0", 21 | "@lerna-lite/publish": "^2.5.0", 22 | "@sapphire/eslint-config": "^4.4.3", 23 | "@sapphire/prettier-config": "^1.4.5", 24 | "@sapphire/ts-config": "^4.0.0", 25 | "@stencil/react-output-target": "^0.5.3", 26 | "@types/node": "^18.16.19", 27 | "@types/react": "^18.2.15", 28 | "@types/react-dom": "^18.2.7", 29 | "@typescript-eslint/eslint-plugin": "^5.62.0", 30 | "@typescript-eslint/parser": "^5.62.0", 31 | "cz-conventional-changelog": "^3.3.0", 32 | "eslint": "^8.45.0", 33 | "eslint-config-prettier": "^8.8.0", 34 | "eslint-plugin-import": "^2.27.5", 35 | "eslint-plugin-jsx-a11y": "^6.7.1", 36 | "eslint-plugin-prettier": "^4.2.1", 37 | "eslint-plugin-react": "^7.32.2", 38 | "eslint-plugin-react-hooks": "^4.6.0", 39 | "gen-esm-wrapper": "^1.1.3", 40 | "husky": "^8.0.3", 41 | "lint-staged": "^13.2.3", 42 | "prettier": "^2.8.8", 43 | "pretty-quick": "^3.1.3", 44 | "react": "^18.2.0", 45 | "react-dom": "^18.2.0", 46 | "replace-in-file": "^6.3.5", 47 | "typescript": "^5.1.6" 48 | }, 49 | "resolutions": { 50 | "ansi-regex": "^5.0.1", 51 | "minimist": "^1.2.8", 52 | "@types/react": "^18.2.15", 53 | "@types/react-dom": "^18.2.7", 54 | "@sapphire/ts-config@^4.0.0": "patch:@sapphire/ts-config@npm%3A4.0.0#./.yarn/patches/@sapphire-ts-config-npm-4.0.0-cfd20d4fc5.patch" 55 | }, 56 | "commitlint": { 57 | "extends": [ 58 | "@commitlint/config-conventional" 59 | ] 60 | }, 61 | "lint-staged": { 62 | "*.{mjs,js,ts,tsx}": "eslint --fix --ext mjs,js,ts,tsx" 63 | }, 64 | "config": { 65 | "commitizen": { 66 | "path": "./node_modules/cz-conventional-changelog" 67 | } 68 | }, 69 | "prettier": "@sapphire/prettier-config", 70 | "packageManager": "yarn@3.6.1" 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed/readme.md: -------------------------------------------------------------------------------- 1 | # discord-embed 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | 9 | | `authorImage` | `author-image` | The author's avatar URL. | `string` | `undefined` | 10 | | `authorName` | `author-name` | The author's name. | `string` | `undefined` | 11 | | `authorUrl` | `author-url` | The URL to open when you click on the author's name. | `string` | `undefined` | 12 | | `color` | `color` | The color to use for the embed's left border. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | 13 | | `embedTitle` | `embed-title` | The embed title. | `string` | `undefined` | 14 | | `image` | `image` | The embed image to use (displayed at the bottom). | `string` | `undefined` | 15 | | `provider` | `provider` | The provider to show above the embed, for example for YouTube videos it will show "YouTube" at the top of the embed (above the author) | `string` | `undefined` | 16 | | `thumbnail` | `thumbnail` | The thumbnail image to use. | `string` | `undefined` | 17 | | `url` | `url` | The URL to open when you click on the embed title. | `string` | `undefined` | 18 | | `video` | `video` | The embed video to use (displayed at the bottom, same slot as the image). | `string` | `undefined` | 19 | 20 | --- 21 | 22 | _Built with [StencilJS](https://stenciljs.com/)_ 23 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-button/discord-button.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop, Watch } from '@stencil/core'; 2 | import Fragment from '../../Fragment'; 3 | import LaunchIcon from '../svgs/launch-icon'; 4 | 5 | @Component({ 6 | tag: 'discord-button', 7 | styleUrl: 'discord-button.css' 8 | }) 9 | export class DiscordButton implements ComponentInterface { 10 | /** 11 | * The DiscordButton element. 12 | */ 13 | @Element() 14 | public el: HTMLElement; 15 | 16 | /** 17 | * The emoji URL to use in the button. 18 | */ 19 | @Prop() 20 | public emoji: string; 21 | 22 | /** 23 | * The name of the emoji used in the button. 24 | */ 25 | @Prop() 26 | public emojiName = 'emoji'; 27 | 28 | /** 29 | * The URL for the button. Setting this will force the button type to be `secondary`. 30 | */ 31 | @Prop() 32 | public url: string; 33 | 34 | /** 35 | * Whether to show the button as disabled. 36 | */ 37 | @Prop() 38 | public disabled = false; 39 | 40 | /** 41 | * The type of button this is, this will change the color of the button. 42 | * Valid values: `primary`, `secondary`, `success`, `destructive`. 43 | */ 44 | @Prop() 45 | public type: 'primary' | 'secondary' | 'success' | 'destructive' = 'secondary'; 46 | 47 | @Watch('type') 48 | public handleType(value: string) { 49 | if (typeof value !== 'string') { 50 | throw new TypeError('DiscordButton `type` prop must be a string.'); 51 | } else if (!['primary', 'secondary', 'success', 'destructive'].includes(value)) { 52 | throw new RangeError("DiscordButton `type` prop must be one of: 'primary', 'secondary', 'success', 'destructive'"); 53 | } 54 | } 55 | 56 | public render() { 57 | const parent: HTMLDiscordActionRowElement = this.el.parentElement as HTMLDiscordActionRowElement; 58 | 59 | if (parent.tagName.toLowerCase() !== 'discord-action-row') { 60 | throw new Error('All components must be direct children of .'); 61 | } 62 | 63 | const content = ( 64 | 65 | {this.emoji && {this.emojiName}} 66 | 67 | 68 | 69 | {this.url && } 70 | 71 | ); 72 | 73 | return this.url && !this.disabled ? ( 74 | 75 | {content} 76 | 77 | ) : ( 78 | {content} 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@derockdev/discord-components-core", 3 | "version": "3.6.1", 4 | "description": "Web components to easily build and display fake Discord messages on your webpages.", 5 | "author": "@derock", 6 | "license": "MIT", 7 | "main": "dist/index.cjs.js", 8 | "module": "dist/index.js", 9 | "es2015": "dist/esm/index.js", 10 | "es2017": "dist/esm/index.js", 11 | "types": "dist/types/index.d.ts", 12 | "unpkg": "dist/derockdev-discord-components-core/derockdev-discord-components-core.esm.js", 13 | "collection:main": "dist/collection/index.js", 14 | "collection": "dist/collection/collection-manifest.json", 15 | "exports": { 16 | ".": [ 17 | { 18 | "types": "./dist/types/index.d.ts", 19 | "require": "./dist/index.cjs.js", 20 | "import": "./dist/index.js" 21 | }, 22 | "./dist/index.cjs.js" 23 | ], 24 | "./loader": [ 25 | { 26 | "types": "./loader/index.d.ts", 27 | "require": "./loader/index.cjs.js", 28 | "import": "./loader/index.js" 29 | }, 30 | "./loader/index.cjs.js" 31 | ], 32 | "./hydrate": [ 33 | { 34 | "require": "./hydrate/index.js", 35 | "import": "./hydrate/index.js" 36 | }, 37 | "./hydrate/index.js" 38 | ] 39 | }, 40 | "sideEffects": [ 41 | "./loader/index.js", 42 | "./loader/index.cjs.js", 43 | "./dist/derockdev-discord-components-core/p-*", 44 | "**/*derockdev-discord-components-core*.js", 45 | "./dist/esm/loader.js", 46 | "./dist/esm/polyfills/*" 47 | ], 48 | "homepage": "https://github.com/itzderock/discord-components/tree/main/packages/core#readme", 49 | "scripts": { 50 | "build": "stencil build --docs && replace-in-file --configFile=scripts/replaceImportInPolyfills.cjs", 51 | "start": "stencil build --dev --watch --serve", 52 | "generate": "stencil generate" 53 | }, 54 | "dependencies": { 55 | "@stencil/core": "^3.4.1", 56 | "clsx": "^1.2.1", 57 | "hex-to-rgba": "^2.0.1", 58 | "highlight.js": "^11.6.0" 59 | }, 60 | "directories": { 61 | "lib": "src" 62 | }, 63 | "files": [ 64 | "dist/", 65 | "loader/", 66 | "hydrate/" 67 | ], 68 | "engines": { 69 | "node": ">=v14.0.0" 70 | }, 71 | "publishConfig": { 72 | "access": "public" 73 | }, 74 | "repository": { 75 | "type": "git", 76 | "url": "git+https://github.com/itzderock/discord-components.git" 77 | }, 78 | "bugs": { 79 | "url": "https://github.com/itzderock/discord-components/issues" 80 | }, 81 | "keywords": [ 82 | "skyra", 83 | "typescript", 84 | "ts", 85 | "yarn", 86 | "discord", 87 | "bot", 88 | "components", 89 | "webcomponents", 90 | "stencil" 91 | ], 92 | "devDependencies": { 93 | "@sapphire/ts-config": "^3.3.4", 94 | "replace-in-file": "^6.3.5", 95 | "tslib": "^2.4.0" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-command/discord-command.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop } from '@stencil/core'; 2 | import { avatars, Profile, profiles } from '../../options'; 3 | import CommandIcon from '../svgs/command-icon'; 4 | 5 | @Component({ 6 | tag: 'discord-command', 7 | styleUrl: 'discord-command.css' 8 | }) 9 | export class DiscordCommand implements ComponentInterface { 10 | /** 11 | * The DiscordCommand element. 12 | */ 13 | @Element() 14 | public el: HTMLElement; 15 | 16 | /** 17 | * The id of the profile data to use. 18 | */ 19 | @Prop() 20 | public profile: string; 21 | 22 | /** 23 | * The message author's username. 24 | * @default 'User' 25 | */ 26 | @Prop() 27 | public author = 'User'; 28 | 29 | /** 30 | * The message author's avatar. Can be an avatar shortcut, relative path, or external link. 31 | */ 32 | @Prop() 33 | public avatar: string; 34 | 35 | /** 36 | * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). 37 | */ 38 | @Prop() 39 | public roleColor: string; 40 | 41 | /** 42 | * The name of the command invoked. 43 | */ 44 | @Prop() 45 | public command: string; 46 | 47 | public render() { 48 | const parent: HTMLDiscordMessageElement = this.el.parentElement as HTMLDiscordMessageElement; 49 | 50 | if (parent.tagName.toLowerCase() !== 'discord-message') { 51 | throw new Error('All components must be direct children of .'); 52 | } 53 | 54 | const resolveAvatar = (avatar: string): string => avatars[avatar] ?? avatar ?? avatars.default; 55 | 56 | const defaultData: Profile = { author: this.author, bot: false, verified: false, server: false, roleColor: this.roleColor }; 57 | const profileData: Profile = Reflect.get(profiles, this.profile) ?? {}; 58 | const profile: Profile = { ...defaultData, ...profileData, ...{ avatar: resolveAvatar(profileData.avatar ?? this.avatar) } }; 59 | 60 | const messageParent: HTMLDiscordMessagesElement = parent.parentElement as HTMLDiscordMessagesElement; 61 | 62 | return ( 63 | 64 | {messageParent.compactMode ? ( 65 |
66 | 67 |
68 | ) : ( 69 | {profile.author} 70 | )} 71 | 72 | {profile.author} 73 | 74 | {' used '} 75 |
{this.command}
76 |
77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reply/readme.md: -------------------------------------------------------------------------------- 1 | # discord-reply 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | 9 | | `attachment` | `attachment` | Whether the referenced message contains attachments. | `boolean` | `false` | 10 | | `author` | `author` | The message author's username. | `string` | `'User'` | 11 | | `avatar` | `avatar` | The message author's avatar. Can be an avatar shortcut, relative path, or external link. | `string` | `undefined` | 12 | | `bot` | `bot` | Whether the message author is a bot or not. Only works if `server` is `false` or `undefined`. | `boolean` | `false` | 13 | | `command` | `command` | Whether the referenced message is from a response of a slash command. | `boolean` | `false` | 14 | | `edited` | `edited` | Whether the message has been edited or not. | `boolean` | `false` | 15 | | `mentions` | `mentions` | Whether this reply pings the original message sender, prepending an "@" on the author's username. | `boolean` | `false` | 16 | | `op` | `op` | Whether the author is the original poster. | `boolean` | `false` | 17 | | `profile` | `profile` | The id of the profile data to use. | `string` | `undefined` | 18 | | `roleColor` | `role-color` | The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | 19 | | `server` | `server` | Whether the message author is a server crosspost webhook or not. Only works if `bot` is `false` or `undefined`. | `boolean` | `false` | 20 | | `verified` | `verified` | Whether the bot is verified or not. Only works if `bot` is `true` | `boolean` | `false` | 21 | 22 | --- 23 | 24 | _Built with [StencilJS](https://stenciljs.com/)_ 25 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-time/discord-time.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h, Host, Prop, State } from '@stencil/core'; 2 | 3 | const DATE_TYPE_FORMATS = { 4 | t: { timeStyle: 'short' }, 5 | T: { timeStyle: 'medium' }, 6 | d: { dateStyle: 'short' }, 7 | D: { dateStyle: 'long' }, 8 | f: { dateStyle: 'long', timeStyle: 'short' }, 9 | F: { dateStyle: 'full', timeStyle: 'short' }, 10 | R: { style: 'long', numeric: 'auto' } 11 | } as const; 12 | 13 | // max: [unit, per unit] 14 | const RELATIVE_DATE_CONVERSION = { 15 | 60000: ['second', 1000], 16 | 3600000: ['minute', 60000], 17 | 86400000: ['hour', 3600000], 18 | 604800000: ['day', 86400000], 19 | 2419200000: ['week', 604800000], 20 | 29030400000: ['month', 2419200000], 21 | 290304000000: ['year', 29030400000] 22 | } as const; 23 | 24 | @Component({ 25 | tag: 'discord-time', 26 | styleUrl: 'discord-time.css' 27 | }) 28 | export class DiscordTime { 29 | /** 30 | * The time to display. 31 | */ 32 | @Prop() 33 | public timestamp: number; 34 | 35 | /** 36 | * The format for the time. 37 | */ 38 | @Prop() 39 | public format: 't' | 'T' | 'f' | 'F' | 'd' | 'D' | 'R' = 't'; 40 | 41 | // Private variables 42 | @State() private time = ''; 43 | private updateInterval: number | undefined; 44 | 45 | public render() { 46 | return {this.time}; 47 | } 48 | 49 | // Lifecycle methods 50 | public connectedCallback() { 51 | this.update(); 52 | } 53 | 54 | public disconnectedCallback() { 55 | window.clearInterval(this.updateInterval); 56 | } 57 | 58 | /** 59 | * Generates a string for the time. 60 | */ 61 | private update() { 62 | const date = new Date(this.timestamp); 63 | 64 | if (this.format === 'R') { 65 | const [formatted, interval] = getRelativeDate(date); 66 | this.time = formatted; 67 | 68 | // Update the time according to the interval 69 | if (this.updateInterval) window.clearInterval(this.updateInterval); 70 | if (interval > -1) this.updateInterval = window.setInterval(() => this.update(), interval); 71 | } else { 72 | this.time = date.toLocaleString(undefined, DATE_TYPE_FORMATS[this.format]); 73 | } 74 | } 75 | } 76 | 77 | // [formatted, updateInterval] 78 | function getRelativeDate(date: Date): [string, number] { 79 | const difference = Date.now() - date.getTime(); 80 | const diffAbsolute = Math.abs(difference); 81 | 82 | const ending = difference < 0 ? 'from now' : 'ago'; 83 | 84 | if (diffAbsolute < 5000) { 85 | return ['Just now', 1000]; 86 | } 87 | 88 | for (const [time, [unit, per]] of Object.entries(RELATIVE_DATE_CONVERSION)) { 89 | if (diffAbsolute < Number(time)) { 90 | const amount = Math.round(diffAbsolute / per); 91 | 92 | return [`${amount} ${unit}${amount === 1 ? '' : 's'} ${ending}`, unit === 'second' ? 1000 : 60 * 1000]; 93 | } 94 | } 95 | 96 | return [`${Math.floor(diffAbsolute / 290304000000)} years ${ending}`, -1]; 97 | } 98 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed-field/discord-embed-field.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop, Watch } from '@stencil/core'; 2 | import clsx from 'clsx'; 3 | import type { Emoji } from '../../options'; 4 | import { getGlobalEmojiUrl } from '../../util'; 5 | 6 | @Component({ 7 | tag: 'discord-embed-field', 8 | styleUrl: 'discord-embed-field.css' 9 | }) 10 | export class DiscordEmbedField implements ComponentInterface { 11 | /** 12 | * The DiscordEmbedField element 13 | */ 14 | @Element() 15 | public el: HTMLElement; 16 | 17 | /** 18 | * The field's title. 19 | */ 20 | @Prop() 21 | public fieldTitle!: string; 22 | 23 | /** 24 | * Whether this field should be displayed inline or not. 25 | */ 26 | @Prop() 27 | public inline = false; 28 | 29 | /** 30 | * The index of this inline field 31 | * @remark This defines the position of this inline field. 1 is left, 2 is middle and 3 is right. 32 | * @oneof [1, 2, 3] 33 | * @default 1 34 | */ 35 | @Prop() 36 | public inlineIndex = 1; 37 | 38 | private validInlineIndices = new Set([1, 2, 3]); 39 | 40 | @Watch('inlineIndex') 41 | public checkInlineIndex(value: DiscordEmbedField['inlineIndex']) { 42 | if (!this.validInlineIndices.has(Number(value))) throw new RangeError('DiscordEmbedField `inlineIndex` prop must be one of: 1, 2, or 3'); 43 | } 44 | 45 | public componentWillRender() { 46 | this.checkInlineIndex(this.inlineIndex); 47 | } 48 | 49 | public render() { 50 | const parent: HTMLDiscordEmbedFieldElement = this.el.parentElement as HTMLDiscordEmbedFieldElement; 51 | 52 | if (parent.tagName.toLowerCase() !== 'discord-embed-fields') { 53 | throw new SyntaxError('All components must be direct children of .'); 54 | } 55 | 56 | const emojiParsedEmbedFieldTitle = this.parseTitle(this.fieldTitle); 57 | 58 | return ( 59 | 70 | {emojiParsedEmbedFieldTitle &&
{[...emojiParsedEmbedFieldTitle]}
} 71 | 72 |
73 | ); 74 | } 75 | 76 | private parseTitle(title?: string) { 77 | if (!title) return null; 78 | 79 | const words = title.split(' '); 80 | 81 | return words.map((word: string, idx: number) => { 82 | const emoji = getGlobalEmojiUrl(word) ?? ({} as Emoji); 83 | let el = ''; 84 | if (emoji.name) { 85 | el = ( 86 | 87 | {emoji.name} 88 |   89 | 90 | ); 91 | } else { 92 | el = idx < words.length - 1 ? `${word} ` : word; 93 | } 94 | return el; 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-thread-message/discord-thread-message.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop } from '@stencil/core'; 2 | import Fragment from '../../Fragment'; 3 | import { avatars, Profile, profiles } from '../../options'; 4 | import VerifiedTick from '../svgs/verified-tick'; 5 | 6 | @Component({ 7 | tag: 'discord-thread-message', 8 | styleUrl: 'discord-thread-message.css' 9 | }) 10 | export class DiscordThreadMessage implements ComponentInterface { 11 | /** 12 | * The DiscordThreadMessage element. 13 | */ 14 | @Element() 15 | public el: HTMLElement; 16 | 17 | /** 18 | * The id of the profile data to use. 19 | */ 20 | @Prop() 21 | public profile: string; 22 | 23 | /** 24 | * The message author's username. 25 | * @default 'User' 26 | */ 27 | @Prop() 28 | public author = 'User'; 29 | 30 | /** 31 | * The message author's avatar. Can be an avatar shortcut, relative path, or external link. 32 | */ 33 | @Prop() 34 | public avatar: string; 35 | 36 | /** 37 | * Whether the message author is a bot or not. 38 | * Only works if `server` is `false` or `undefined`. 39 | */ 40 | @Prop() 41 | public bot = false; 42 | 43 | /** 44 | * Whether the message author is a server crosspost webhook or not. 45 | * Only works if `bot` is `false` or `undefined`. 46 | */ 47 | @Prop() 48 | public server = false; 49 | 50 | /** 51 | * Whether the bot is verified or not. 52 | * Only works if `bot` is `true` 53 | */ 54 | @Prop() 55 | public verified = false; 56 | 57 | /** 58 | * Whether the message has been edited or not. 59 | */ 60 | @Prop() 61 | public edited = false; 62 | 63 | /** 64 | * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). 65 | */ 66 | @Prop() 67 | public roleColor: string; 68 | 69 | /** 70 | * The relative timestamp of the message. 71 | */ 72 | @Prop() 73 | public relativeTimestamp = '1m ago'; 74 | 75 | public render() { 76 | const resolveAvatar = (avatar: string): string => avatars[avatar] ?? avatar ?? avatars.default; 77 | 78 | const defaultData: Profile = { author: this.author, bot: this.bot, verified: this.verified, server: this.server, roleColor: this.roleColor }; 79 | const profileData: Profile = Reflect.get(profiles, this.profile) ?? {}; 80 | const profile: Profile = { ...defaultData, ...profileData, ...{ avatar: resolveAvatar(profileData.avatar ?? this.avatar) } }; 81 | 82 | return ( 83 | 84 | {profile.author} 85 | 86 | {profile.bot && !profile.server && ( 87 | 88 | {profile.verified && } 89 | Bot 90 | 91 | )} 92 | {profile.server && !profile.bot && Server} 93 | 94 | 95 | {profile.author} 96 | 97 |
98 | 99 | {this.edited ? (edited) : ''} 100 |
101 | {this.relativeTimestamp} 102 |
103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-invite/discord-invite.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Prop } from '@stencil/core'; 2 | import { defaultDiscordAvatars } from '../../options'; 3 | import GuildBadge from '../svgs/guild-badge'; 4 | import PartnerBadgeOverlay from '../svgs/partner-badge-overlay'; 5 | import VerifiedBadgeOverlay from '../svgs/verified-badge-overlay'; 6 | 7 | @Component({ 8 | tag: 'discord-invite', 9 | styleUrl: 'discord-invite.css' 10 | }) 11 | export class DiscordInvite implements ComponentInterface { 12 | /** 13 | * The DiscordInvite element. 14 | */ 15 | @Element() 16 | public el: HTMLElement; 17 | 18 | /** 19 | * The server icon to display for the invite. 20 | */ 21 | @Prop() 22 | public icon = defaultDiscordAvatars.blue; 23 | 24 | /** 25 | * The server's name. 26 | * @default 'Discord Server' 27 | */ 28 | @Prop() 29 | public name = 'Discord Server'; 30 | 31 | /** 32 | * The URL to open when you click on the join button. 33 | */ 34 | @Prop() 35 | public url: string; 36 | 37 | /** 38 | * The number of members online on the server. 39 | * @default 0 40 | */ 41 | @Prop() 42 | public online = 0; 43 | 44 | /** 45 | * The number of members on the server. 46 | * @default 0 47 | */ 48 | @Prop() 49 | public members = 0; 50 | 51 | /** 52 | * Whether the server is verified. 53 | * Only works if `partnered` is `false` or `undefined`. 54 | */ 55 | @Prop() 56 | public verified = false; 57 | 58 | /** 59 | * Whether the server is partnered. 60 | * Only works if `verified` is `false` or `undefined`. 61 | */ 62 | @Prop() 63 | public partnered = false; 64 | 65 | /** 66 | * Invitation embed title. 67 | * @default "You've been invited to join a server" 68 | */ 69 | @Prop() 70 | public inviteTitle = "You've been invited to join a server"; 71 | 72 | /** 73 | * The join button. 74 | * @default 'Join' 75 | */ 76 | @Prop() 77 | public joinBtn = 'Join'; 78 | 79 | public render() { 80 | return ( 81 |
82 |
{this.inviteTitle}
83 |
84 | {this.name} 85 |
86 |
87 | {((this.verified && !this.partnered) || (!this.verified && this.partnered)) && ( 88 |
89 | 93 |
94 | {this.partnered ? : } 95 |
96 |
97 | )} 98 | {this.name} 99 |
100 |
101 | 102 | {this.online.toLocaleString()} Online 103 | 104 | {this.members.toLocaleString()} Members 105 |
106 |
107 | 108 | {this.joinBtn} 109 | 110 |
111 |
112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/react/src/react-component-lib/createComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { createElement } from 'react'; 2 | 3 | import { attachProps, camelToDashCase, createForwardRef, dashToPascalCase, isCoveredByReact, mergeRefs } from './utils'; 4 | 5 | export interface HTMLStencilElement extends HTMLElement { 6 | componentOnReady(): Promise; 7 | } 8 | 9 | interface StencilReactInternalProps extends React.HTMLAttributes { 10 | forwardedRef: React.RefObject; 11 | ref?: React.Ref; 12 | } 13 | 14 | export const createReactComponent = ( 15 | tagName: string, 16 | ReactComponentContext?: React.Context, 17 | manipulatePropsFunction?: (originalProps: StencilReactInternalProps, propsToPass: any) => ExpandedPropsTypes, 18 | defineCustomElement?: () => void 19 | ) => { 20 | if (defineCustomElement !== undefined) { 21 | defineCustomElement(); 22 | } 23 | 24 | const displayName = dashToPascalCase(tagName); 25 | const ReactComponent = class extends React.Component> { 26 | componentEl!: ElementType; 27 | 28 | setComponentElRef = (element: ElementType) => { 29 | this.componentEl = element; 30 | }; 31 | 32 | constructor(props: StencilReactInternalProps) { 33 | super(props); 34 | } 35 | 36 | componentDidMount() { 37 | this.componentDidUpdate(this.props); 38 | } 39 | 40 | componentDidUpdate(prevProps: StencilReactInternalProps) { 41 | attachProps(this.componentEl, this.props, prevProps); 42 | } 43 | 44 | render() { 45 | const { children, forwardedRef, style, className, ref, ...cProps } = this.props; 46 | 47 | let propsToPass = Object.keys(cProps).reduce((acc: any, name) => { 48 | const value = (cProps as any)[name]; 49 | 50 | if (name.startsWith('on') && name[2] === name[2].toUpperCase()) { 51 | const eventName = name.substring(2).toLowerCase(); 52 | if (typeof document !== 'undefined' && isCoveredByReact(eventName)) { 53 | acc[name] = value; 54 | } 55 | } else { 56 | // we should only render strings, booleans, and numbers as attrs in html. 57 | // objects, functions, arrays etc get synced via properties on mount. 58 | const type = typeof value; 59 | 60 | if (type === 'string' || type === 'boolean' || type === 'number') { 61 | acc[camelToDashCase(name)] = value; 62 | } 63 | } 64 | return acc; 65 | }, {}); 66 | 67 | if (manipulatePropsFunction) { 68 | propsToPass = manipulatePropsFunction(this.props, propsToPass); 69 | } 70 | 71 | const newProps: Omit, 'forwardedRef'> = { 72 | ...propsToPass, 73 | ref: mergeRefs(forwardedRef, this.setComponentElRef), 74 | style 75 | }; 76 | 77 | /** 78 | * We use createElement here instead of 79 | * React.createElement to work around a 80 | * bug in Vite (https://github.com/vitejs/vite/issues/6104). 81 | * React.createElement causes all elements to be rendered 82 | * as instead of the actual Web Component. 83 | */ 84 | return createElement(tagName, newProps, children); 85 | } 86 | 87 | static get displayName() { 88 | return displayName; 89 | } 90 | }; 91 | 92 | // If context was passed to createReactComponent then conditionally add it to the Component Class 93 | if (ReactComponentContext) { 94 | ReactComponent.contextType = ReactComponentContext; 95 | } 96 | 97 | return createForwardRef(ReactComponent, displayName); 98 | }; 99 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-message/readme.md: -------------------------------------------------------------------------------- 1 | # discord-message 2 | 3 | 4 | 5 | ## Properties 6 | 7 | | Property | Attribute | Description | Type | Default | 8 | | ------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------------ | 9 | | `author` | `author` | The message author's username. | `string` | `'User'` | 10 | | `avatar` | `avatar` | The message author's avatar. Can be an avatar shortcut, relative path, or external link. | `string` | `undefined` | 11 | | `bot` | `bot` | Whether the message author is a bot or not. Only works if `server` is `false` or `undefined`. | `boolean` | `false` | 12 | | `edited` | `edited` | Whether the message has been edited or not. | `boolean` | `false` | 13 | | `ephemeral` | `ephemeral` | Whether to make this message ephemeral. | `boolean` | `false` | 14 | | `highlight` | `highlight` | Whether to highlight this message. | `boolean` | `false` | 15 | | `op` | `op` | Whether the author is the original poster. | `boolean` | `false` | 16 | | `profile` | `profile` | The id of the profile data to use. | `string` | `undefined` | 17 | | `roleColor` | `role-color` | The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | 18 | | `roleIcon` | `role-icon` | The message author's role icon URL. | `string` | `undefined` | 19 | | `roleName` | `role-name` | The name of the role to use as alternative image text. | `string` | `undefined` | 20 | | `server` | `server` | Whether the message author is a server crosspost webhook or not. Only works if `bot` is `false` or `undefined`. | `boolean` | `false` | 21 | | `timestamp` | `timestamp` | The timestamp to use for the message date. | `Date \| null \| string` | `new Date()` | 22 | | `twentyFour` | `twenty-four` | Whether to use 24-hour format for the timestamp. | `boolean` | `false` | 23 | | `verified` | `verified` | Whether the bot is verified or not. Only works if `bot` is `true` | `boolean` | `false` | 24 | 25 | --- 26 | 27 | _Built with [StencilJS](https://stenciljs.com/)_ 28 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-system-message/discord-system-message.css: -------------------------------------------------------------------------------- 1 | .discord-system-message { 2 | color: #8e9297; 3 | display: flex; 4 | font-weight: 400; 5 | font-size: 1rem; 6 | font-family: Whitney, 'Source Sans Pro', ui-sans-serif, system-ui, -apple-system, 'system-ui', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 7 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 8 | padding: 0px 1em; 9 | 10 | position: relative; 11 | word-wrap: break-word; 12 | -webkit-user-select: text; 13 | -moz-user-select: text; 14 | -ms-user-select: text; 15 | user-select: text; 16 | -webkit-box-flex: 0; 17 | -ms-flex: 0 0 auto; 18 | flex: 0 0 auto; 19 | padding-right: 0; 20 | min-height: 1.375rem; 21 | padding-right: 48px !important; 22 | margin-top: 1.0625rem; 23 | } 24 | 25 | .discord-light-theme .discord-system-message { 26 | color: #2e3338; 27 | border-color: #eceeef; 28 | } 29 | 30 | .discord-system-message.discord-channel-name-change { 31 | color: #fff; 32 | } 33 | 34 | .discord-light-theme .discord-system-message.discord-channel-name-change { 35 | color: #060607; 36 | } 37 | 38 | .discord-system-message.discord-boost-system-message svg { 39 | color: #ff73fa; 40 | } 41 | 42 | .discord-system-message.discord-alert-system-message svg { 43 | color: #faa81a; 44 | } 45 | 46 | .discord-system-message.discord-error-system-message svg { 47 | color: #faa81a; 48 | } 49 | 50 | .discord-system-message:first-child { 51 | margin-top: 0.5rem; 52 | } 53 | 54 | .discord-system-message:last-child { 55 | margin-bottom: 0.5rem; 56 | border-bottom-width: 0; 57 | } 58 | 59 | .discord-system-message .discord-message-icon { 60 | margin-right: 16px; 61 | margin-top: 5px; 62 | min-width: 40px; 63 | display: flex; 64 | align-items: flex-start; 65 | justify-content: center; 66 | } 67 | 68 | .discord-system-message .discord-message-icon svg { 69 | width: 16px; 70 | height: 16px; 71 | } 72 | 73 | .discord-system-message .discord-message-timestamp { 74 | color: #72767d; 75 | font-size: 12px; 76 | margin-left: 3px; 77 | } 78 | 79 | .discord-light-theme .discord-system-message .discord-message-timestamp { 80 | color: #747f8d; 81 | } 82 | 83 | .discord-system-message .discord-message-system-edited { 84 | color: #72767d; 85 | font-size: 10px; 86 | } 87 | 88 | .discord-light-theme .discord-system-message .discord-message-edited { 89 | color: #99aab5; 90 | } 91 | 92 | .discord-system-message .discord-message-content { 93 | width: 100%; 94 | line-height: 160%; 95 | font-weight: normal; 96 | padding-top: 2px; 97 | display: flex; 98 | flex-direction: column; 99 | } 100 | 101 | .discord-system-message .discord-message-content i { 102 | font-style: normal; 103 | cursor: pointer; 104 | color: white; 105 | font-weight: 500; 106 | } 107 | 108 | .discord-light-theme .discord-system-message .discord-message-content i { 109 | color: #060607; 110 | } 111 | 112 | .discord-system-message .discord-message-content i:hover { 113 | text-decoration: underline; 114 | } 115 | 116 | .discord-system-message:hover { 117 | background-color: rgba(4, 4, 5, 0.07); 118 | } 119 | 120 | .discord-light-theme .discord-system-message:hover { 121 | background-color: rgba(6, 6, 7, 0.02); 122 | } 123 | 124 | .discord-system-message.discord-system-message-has-thread:after { 125 | width: 2rem; 126 | left: 2.2rem; 127 | top: 1.75rem; 128 | border-left: 2px solid #4f545c; 129 | border-bottom: 2px solid #4f545c; 130 | border-bottom-left-radius: 8px; 131 | bottom: 29px; 132 | content: ''; 133 | position: absolute; 134 | } 135 | 136 | .discord-light-theme .discord-system-message.discord-system-message-has-thread:after { 137 | border-color: #747f8d; 138 | } 139 | 140 | @import '../author-info/author-info.css'; 141 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-mention/discord-mention.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop, Watch } from '@stencil/core'; 2 | import hexToRgba from 'hex-to-rgba'; 3 | import ChannelForum from '../svgs/channel-forum'; 4 | import ChannelIcon from '../svgs/channel-icon'; 5 | import ChannelThread from '../svgs/channel-thread'; 6 | import LockedVoiceChannel from '../svgs/locked-voice-channel'; 7 | import VoiceChannel from '../svgs/voice-channel'; 8 | 9 | @Component({ 10 | tag: 'discord-mention', 11 | styleUrl: 'discord-mention.css' 12 | }) 13 | export class DiscordMention implements ComponentInterface { 14 | /** 15 | * The DiscordMention element 16 | */ 17 | @Element() 18 | public el: HTMLElement; 19 | 20 | /** 21 | * Whether this entire message block should be highlighted (to emulate the "logged in user" being pinged). 22 | */ 23 | @Prop() 24 | public highlight = false; 25 | 26 | /** 27 | * The color to use for this mention. Only works for role mentions and must be in hex format. 28 | */ 29 | @Prop() 30 | public color: string; 31 | 32 | /** 33 | * The type of mention this should be. This will prepend the proper prefix character. 34 | * Valid values: `user`, `channel`, `role`, `voice`, `locked`, `thread`, `forum`, and `slash`. 35 | */ 36 | @Prop() 37 | public type: 'user' | 'channel' | 'role' | 'voice' | 'locked' | 'thread' | 'forum' | 'slash' = 'user'; 38 | 39 | @Watch('type') 40 | public handleType(value: string) { 41 | if (typeof value !== 'string') { 42 | throw new TypeError('DiscordMention `type` prop must be a string.'); 43 | } else if (!['user', 'channel', 'role', 'voice', 'locked', 'thread', 'forum', 'slash'].includes(value)) { 44 | throw new RangeError( 45 | "DiscordMention `type` prop must be one of: 'user', 'channel', 'role', 'voice', 'locked', 'thread', 'forum', 'slash'" 46 | ); 47 | } 48 | } 49 | 50 | public componentWillRender() { 51 | this.handleType(this.type); 52 | } 53 | 54 | public componentDidLoad() { 55 | if (this.color && this.type === 'role') { 56 | this.el.addEventListener('mouseover', this.setHoverColor.bind(this)); 57 | this.el.addEventListener('mouseout', this.resetHoverColor.bind(this)); 58 | } 59 | } 60 | 61 | public disconnectedCallback() { 62 | if (this.color && this.type === 'role') { 63 | this.el.removeEventListener('mouseover', this.setHoverColor.bind(this)); 64 | this.el.removeEventListener('mouseout', this.resetHoverColor.bind(this)); 65 | } 66 | } 67 | 68 | public setHoverColor() { 69 | this.el.style.backgroundColor = hexToRgba(this.color, 0.3); 70 | } 71 | 72 | public resetHoverColor() { 73 | this.el.style.backgroundColor = hexToRgba(this.color, 0.1); 74 | } 75 | 76 | public render() { 77 | const { color, type }: { color?: string; type?: string } = this; 78 | 79 | const colorStyle: { 80 | color?: string; 81 | 'background-color'?: string; 82 | } = !color || type !== 'role' ? {} : { color, 'background-color': hexToRgba(color, 0.1) }; 83 | 84 | let mentionPrepend = ''; 85 | 86 | switch (this.type) { 87 | case 'channel': 88 | mentionPrepend = ; 89 | break; 90 | case 'user': 91 | case 'role': 92 | mentionPrepend = '@'; 93 | break; 94 | case 'voice': 95 | mentionPrepend = ; 96 | break; 97 | case 'locked': 98 | mentionPrepend = ; 99 | break; 100 | case 'thread': 101 | mentionPrepend = ; 102 | break; 103 | case 'forum': 104 | mentionPrepend = ; 105 | break; 106 | case 'slash': 107 | mentionPrepend = '/'; 108 | break; 109 | } 110 | 111 | return ( 112 | 113 | {mentionPrepend} 114 | 115 | 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reply/discord-reply.css: -------------------------------------------------------------------------------- 1 | .discord-replied-message { 2 | color: #b9bbbe; 3 | display: flex; 4 | font-size: 0.875rem; 5 | font-family: Whitney, 'Source Sans Pro', ui-sans-serif, system-ui, -apple-system, 'system-ui', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 6 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 7 | 8 | padding-top: 2px; 9 | margin-left: 56px; 10 | margin-bottom: 4px; 11 | align-items: center; 12 | line-height: 1.125rem; 13 | position: relative; 14 | white-space: pre; 15 | user-select: none; 16 | } 17 | 18 | .discord-light-theme .discord-replied-message { 19 | color: #4f5660; 20 | } 21 | 22 | .discord-compact-mode .discord-replied-message { 23 | margin-left: 62px; 24 | margin-bottom: 0; 25 | } 26 | 27 | .discord-replied-message:before { 28 | content: ''; 29 | display: block; 30 | position: absolute; 31 | top: 50%; 32 | right: 100%; 33 | bottom: 0; 34 | left: -36px; 35 | margin-right: 4px; 36 | margin-top: -1px; 37 | margin-left: -1px; 38 | margin-bottom: -2px; 39 | border-left: 2px solid #4f545c; 40 | border-bottom: 0 solid #4f545c; 41 | border-right: 0 solid #4f545c; 42 | border-top: 2px solid #4f545c; 43 | border-top-left-radius: 6px; 44 | } 45 | 46 | .discord-light-theme .discord-replied-message:before { 47 | border-color: #747f8d; 48 | } 49 | 50 | .discord-replied-message .discord-replied-message-avatar, 51 | .discord-replied-message .discord-reply-badge { 52 | -webkit-box-flex: 0; 53 | -ms-flex: 0 0 auto; 54 | flex: 0 0 auto; 55 | width: 16px; 56 | height: 16px; 57 | border-radius: 50%; 58 | user-select: none; 59 | margin-right: 0.25rem; 60 | } 61 | 62 | .discord-replied-message .discord-reply-badge { 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | color: #b9bbbe; 67 | background: #202225; 68 | } 69 | 70 | .discord-light-theme .discord-replied-message .discord-reply-badge { 71 | color: #4f5660; 72 | background: #e3e5e8; 73 | } 74 | 75 | .discord-replied-message .discord-application-tag { 76 | background-color: hsl(235, 85.6%, 64.7%); 77 | color: #fff; 78 | font-size: 0.625rem; 79 | margin-right: 0.25rem; 80 | line-height: 100%; 81 | text-transform: uppercase; 82 | /* Use flex layout to ensure both verified icon and "BOT" text are aligned to center */ 83 | display: flex; 84 | align-items: center; 85 | /* Styling taken through Inspect Element on Discord client for Windows */ 86 | height: 0.9375rem; 87 | padding: 0 0.275rem; 88 | margin-top: 0.075em; 89 | border-radius: 0.1875rem; 90 | } 91 | 92 | .discord-replied-message .discord-application-tag .discord-application-tag-verified { 93 | width: 0.9375rem; 94 | height: 0.9375rem; 95 | margin-left: -0.1rem; 96 | } 97 | 98 | .discord-replied-message .discord-application-tag.discord-application-tag-op { 99 | background-color: #c9cdfb; 100 | color: #4752c4; 101 | border-radius: 0.4rem; 102 | } 103 | 104 | .discord-replied-message .discord-replied-message-username { 105 | flex-shrink: 0; 106 | font-size: inherit; 107 | line-height: inherit; 108 | margin-right: 0.25rem; 109 | opacity: 0.64; 110 | font-weight: 500; 111 | color: #fff; 112 | } 113 | 114 | .discord-replied-message .discord-replied-message-content { 115 | color: inherit; 116 | font-size: inherit; 117 | line-height: inherit; 118 | white-space: pre; 119 | text-overflow: ellipsis; 120 | user-select: none; 121 | cursor: pointer; 122 | overflow-x: hidden; 123 | } 124 | 125 | .discord-replied-message .discord-replied-message-content:hover { 126 | color: #fff; 127 | } 128 | 129 | .discord-light-theme .discord-replied-message .discord-replied-message-content:hover { 130 | color: #000; 131 | } 132 | 133 | .discord-replied-message .discord-replied-message-content .discord-message-edited { 134 | margin-left: 0.25rem; 135 | } 136 | 137 | .discord-replied-message .discord-replied-message-content-icon { 138 | -webkit-box-flex: 0; 139 | -ms-flex: 0 0 auto; 140 | flex: 0 0 auto; 141 | width: 20px; 142 | height: 20px; 143 | margin-left: 4px; 144 | } 145 | 146 | @import '../author-info/author-info.css'; 147 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-attachment/discord-attachment.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop } from '@stencil/core'; 2 | 3 | enum DiscordAttachmentType { 4 | IMAGE = 'image', 5 | VIDEO = 'video', 6 | AUDIO = 'audio', 7 | FILE = 'file' 8 | } 9 | 10 | @Component({ 11 | tag: 'discord-attachment', 12 | styleUrl: 'discord-attachment.css' 13 | }) 14 | export class DiscordAttachment implements ComponentInterface { 15 | /** 16 | * The DiscordEmbed element. 17 | */ 18 | @Element() 19 | public el: HTMLElement; 20 | 21 | /** 22 | * The URL for the image attachment 23 | * @important Should be a valid image URL, i.e. matching the regex `/\.(bmp|jpe?g|png|gif|webp|tiff)$/i` 24 | */ 25 | @Prop() 26 | public url: string; 27 | 28 | /** 29 | * The type of file the attachment is. 30 | * 'image' | 'video' | 'audio' | 'file' 31 | */ 32 | @Prop() 33 | public type: 'image' | 'video' | 'audio' | 'file'; 34 | 35 | /** 36 | * The size of the file. 37 | */ 38 | @Prop() 39 | public size: string; 40 | 41 | /** 42 | * The height of the image in pixels 43 | */ 44 | @Prop() 45 | public height?: number; 46 | 47 | /** 48 | * The width of the image in pixels 49 | */ 50 | @Prop() 51 | public width?: number; 52 | 53 | /** 54 | * The alt text to show in case the image was unable to load 55 | * @default 'discord attachment' 56 | */ 57 | @Prop() 58 | public alt? = 'discord attachment'; 59 | 60 | public render() { 61 | switch (this.type) { 62 | case DiscordAttachmentType.IMAGE: 63 | return ( 64 | 65 |
66 | {this.alt} 67 |
68 |
69 | ); 70 | 71 | case DiscordAttachmentType.VIDEO: 72 | return ( 73 | 74 |
75 |
77 |
78 | ); 79 | 80 | case DiscordAttachmentType.AUDIO: 81 | return ( 82 | 83 | 85 | ); 86 | 87 | case DiscordAttachmentType.FILE: 88 | default: 89 | return ( 90 | 91 |
92 | 93 | 97 | 101 | 102 |
103 | 104 |
105 | 110 | 111 |
{this.size}
112 |
113 | 114 | 126 |
127 | ); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-system-message/discord-system-message.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop, Watch } from '@stencil/core'; 2 | import clsx from 'clsx'; 3 | import { DiscordTimestamp, handleTimestamp } from '../../util'; 4 | import Boost from '../svgs/boost'; 5 | import DMCall from '../svgs/dm-call'; 6 | import DMEdit from '../svgs/dm-edit'; 7 | import DMMissedCall from '../svgs/dm-missed-call'; 8 | import Pin from '../svgs/pin'; 9 | import SystemAlert from '../svgs/system-alert'; 10 | import SystemError from '../svgs/system-error'; 11 | import Thread from '../svgs/thread'; 12 | import UserJoin from '../svgs/user-join'; 13 | import UserLeave from '../svgs/user-leave'; 14 | 15 | @Component({ 16 | tag: 'discord-system-message', 17 | styleUrl: 'discord-system-message.css' 18 | }) 19 | export class DiscordSystemMessage implements ComponentInterface { 20 | /** 21 | * The DiscordSystemMessage element. 22 | */ 23 | @Element() 24 | public el: HTMLElement; 25 | 26 | /** 27 | * The timestamp to use for the message date. 28 | */ 29 | @Prop({ mutable: true, reflect: true }) 30 | public timestamp: DiscordTimestamp = new Date(); 31 | 32 | /** 33 | * The type of system message this is, this will change the icon shown. 34 | * Valid values: `join`, `leave`, `call`, `missed-call`, `boost`, `edit`, `thread`, `pin`, `alert`, and `error`. 35 | */ 36 | @Prop() 37 | public type: 'join' | 'leave' | 'call' | 'missed-call' | 'boost' | 'edit' | 'thread' | 'pin' | 'alert' | 'error' = 'join'; 38 | 39 | /** 40 | * Whether this message is to show channel name changes, used to match Discord's style. 41 | */ 42 | @Prop() 43 | public channelName = false; 44 | 45 | @Watch('type') 46 | public handleType(value: string) { 47 | if (typeof value !== 'string') { 48 | throw new TypeError('DiscordSystemMessage `type` prop must be a string.'); 49 | } else if (!['join', 'leave', 'call', 'missed-call', 'boost', 'edit', 'thread', 'pin', 'alert', 'error'].includes(value)) { 50 | throw new RangeError( 51 | "DiscordSystemMessage `type` prop must be one of: 'join', 'leave', 'call', 'missed-call', 'boost', 'edit', 'pin', 'thread' 'alert', 'error'" 52 | ); 53 | } 54 | } 55 | 56 | @Watch('timestamp') 57 | public updateTimestamp(value: DiscordTimestamp): string | null { 58 | return handleTimestamp(value); 59 | } 60 | 61 | public componentWillRender() { 62 | this.timestamp = handleTimestamp(this.timestamp); 63 | } 64 | 65 | public render() { 66 | const parent: HTMLDiscordMessagesElement = this.el.parentElement as HTMLDiscordMessagesElement; 67 | 68 | if (parent.tagName.toLowerCase() !== 'discord-messages') { 69 | throw new Error('All components must be direct children of .'); 70 | } 71 | 72 | let icon = ''; 73 | 74 | switch (this.type) { 75 | case 'join': 76 | icon = ; 77 | break; 78 | case 'leave': 79 | icon = ; 80 | break; 81 | case 'call': 82 | icon = ; 83 | break; 84 | case 'missed-call': 85 | icon = ; 86 | break; 87 | case 'edit': 88 | icon = ; 89 | break; 90 | case 'boost': 91 | icon = ; 92 | break; 93 | case 'thread': 94 | icon = ; 95 | break; 96 | case 'alert': 97 | icon = ; 98 | break; 99 | case 'error': 100 | icon = ; 101 | break; 102 | case 'pin': 103 | icon = ; 104 | break; 105 | } 106 | 107 | const hasThread: boolean = 108 | // @ts-expect-error ts doesn't understand this 109 | Array.from(this.el.children).some((child: HTMLDiscordThreadElement): boolean => { 110 | return child.tagName.toLowerCase() === 'discord-thread'; 111 | }); 112 | 113 | return ( 114 | 120 |
{icon}
121 |
122 | 123 | 124 | {this.timestamp} 125 | 126 | 127 | 128 |
129 |
130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/react/src/react-component-lib/utils/attachProps.ts: -------------------------------------------------------------------------------- 1 | import { camelToDashCase } from './case'; 2 | 3 | export const attachProps = (node: HTMLElement, newProps: any, oldProps: any = {}) => { 4 | // some test frameworks don't render DOM elements, so we test here to make sure we are dealing with DOM first 5 | if (node instanceof Element) { 6 | // add any classes in className to the class list 7 | const className = getClassName(node.classList, newProps, oldProps); 8 | if (className !== '') { 9 | node.className = className; 10 | } 11 | 12 | Object.keys(newProps).forEach((name) => { 13 | if (name === 'children' || name === 'style' || name === 'ref' || name === 'class' || name === 'className' || name === 'forwardedRef') { 14 | return; 15 | } 16 | if (name.startsWith('on') && name[2] === name[2].toUpperCase()) { 17 | const eventName = name.substring(2); 18 | const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); 19 | 20 | if (!isCoveredByReact(eventNameLc)) { 21 | syncEvent(node, eventNameLc, newProps[name]); 22 | } 23 | } else { 24 | (node as any)[name] = newProps[name]; 25 | const propType = typeof newProps[name]; 26 | if (propType === 'string') { 27 | node.setAttribute(camelToDashCase(name), newProps[name]); 28 | } 29 | } 30 | }); 31 | } 32 | }; 33 | 34 | export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: any) => { 35 | const newClassProp: string = newProps.className || newProps.class; 36 | const oldClassProp: string = oldProps.className || oldProps.class; 37 | // map the classes to Maps for performance 38 | const currentClasses = arrayToMap(classList); 39 | const incomingPropClasses = arrayToMap(newClassProp ? newClassProp.split(' ') : []); 40 | const oldPropClasses = arrayToMap(oldClassProp ? oldClassProp.split(' ') : []); 41 | const finalClassNames: string[] = []; 42 | // loop through each of the current classes on the component 43 | // to see if it should be a part of the classNames added 44 | currentClasses.forEach((currentClass) => { 45 | if (incomingPropClasses.has(currentClass)) { 46 | // add it as its already included in classnames coming in from newProps 47 | finalClassNames.push(currentClass); 48 | incomingPropClasses.delete(currentClass); 49 | } else if (!oldPropClasses.has(currentClass)) { 50 | // add it as it has NOT been removed by user 51 | finalClassNames.push(currentClass); 52 | } 53 | }); 54 | incomingPropClasses.forEach((s) => finalClassNames.push(s)); 55 | return finalClassNames.join(' '); 56 | }; 57 | 58 | /** 59 | * Transforms a React event name to a browser event name. 60 | */ 61 | export const transformReactEventName = (eventNameSuffix: string) => { 62 | switch (eventNameSuffix) { 63 | case 'doubleclick': 64 | return 'dblclick'; 65 | } 66 | return eventNameSuffix; 67 | }; 68 | 69 | /** 70 | * Checks if an event is supported in the current execution environment. 71 | * @license Modernizr 3.0.0pre (Custom Build) | MIT 72 | */ 73 | export const isCoveredByReact = (eventNameSuffix: string) => { 74 | if (typeof document === 'undefined') { 75 | return true; 76 | } 77 | const eventName = 'on' + transformReactEventName(eventNameSuffix); 78 | let isSupported = eventName in document; 79 | 80 | if (!isSupported) { 81 | const element = document.createElement('div'); 82 | element.setAttribute(eventName, 'return;'); 83 | isSupported = typeof (element as any)[eventName] === 'function'; 84 | } 85 | 86 | return isSupported; 87 | }; 88 | 89 | export const syncEvent = ( 90 | node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined } }, 91 | eventName: string, 92 | newEventHandler?: (e: Event) => any 93 | ) => { 94 | const eventStore = node.__events || (node.__events = {}); 95 | const oldEventHandler = eventStore[eventName]; 96 | 97 | // Remove old listener so they don't double up. 98 | if (oldEventHandler) { 99 | node.removeEventListener(eventName, oldEventHandler); 100 | } 101 | 102 | // Bind new listener. 103 | node.addEventListener( 104 | eventName, 105 | (eventStore[eventName] = function handler(e: Event) { 106 | if (newEventHandler) { 107 | newEventHandler.call(this, e); 108 | } 109 | }) 110 | ); 111 | }; 112 | 113 | const arrayToMap = (arr: string[] | DOMTokenList) => { 114 | const map = new Map(); 115 | (arr as string[]).forEach((s: string) => map.set(s, s)); 116 | return map; 117 | }; 118 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-embed/discord-embed.css: -------------------------------------------------------------------------------- 1 | .discord-embed { 2 | color: #dcddde; 3 | display: flex; 4 | font-size: 13px; 5 | line-height: 150%; 6 | margin-bottom: 2px; 7 | margin-top: 2px; 8 | } 9 | 10 | .discord-light-theme .discord-embed { 11 | color: #2e3338; 12 | } 13 | 14 | .discord-embed .discord-left-border { 15 | background-color: #202225; 16 | border-radius: 4px 0 0 4px; 17 | flex-shrink: 0; 18 | width: 4px; 19 | } 20 | 21 | .discord-light-theme .discord-embed .discord-left-border { 22 | background-color: #e3e5e8; 23 | } 24 | 25 | .discord-embed .discord-embed-root { 26 | display: grid; 27 | grid-auto-flow: row; 28 | grid-row-gap: 0.25rem; 29 | min-height: 0; 30 | min-width: 0; 31 | text-indent: 0; 32 | } 33 | 34 | .discord-embed .discord-embed-wrapper { 35 | background-color: #2f3136; 36 | max-width: 520px; 37 | border: 1px solid rgba(46, 48, 54, 0.6); 38 | border-radius: 0 4px 4px 0; 39 | justify-self: start; 40 | align-self: start; 41 | display: grid; 42 | box-sizing: border-box; 43 | } 44 | 45 | .discord-light-theme .discord-embed .discord-embed-wrapper { 46 | background-color: rgba(249, 249, 249, 0.3); 47 | border-color: rgba(205, 205, 205, 0.3); 48 | } 49 | 50 | .discord-embed .discord-embed-wrapper .discord-embed-grid { 51 | display: inline-grid; 52 | grid-template-columns: auto -webkit-min-content; 53 | grid-template-columns: auto min-content; 54 | grid-template-columns: auto; 55 | grid-template-rows: auto; 56 | padding: 0.5rem 1rem 1rem 0.75rem; 57 | } 58 | 59 | .discord-embed .discord-embed-thumbnail { 60 | border-radius: 4px; 61 | flex-shrink: 0; 62 | grid-column: 2/2; 63 | grid-row: 1/8; 64 | justify-self: end; 65 | margin-left: 16px; 66 | margin-top: 8px; 67 | max-height: 80px; 68 | max-width: 80px; 69 | object-fit: contain; 70 | object-position: top center; 71 | } 72 | 73 | .discord-embed .discord-embed-author { 74 | -webkit-box-align: center; 75 | align-items: center; 76 | color: #fff; 77 | font-size: 14px; 78 | display: flex; 79 | font-weight: 600; 80 | grid-column: 1 / 1; 81 | margin-top: 8px; 82 | min-width: 0; 83 | } 84 | 85 | .discord-light-theme .discord-embed .discord-embed-author { 86 | color: #4f545c; 87 | } 88 | 89 | .discord-embed .discord-embed-author a { 90 | color: #fff; 91 | font-weight: 600; 92 | } 93 | 94 | .discord-light-theme .discord-embed .discord-embed-author a { 95 | color: #4f545c; 96 | } 97 | 98 | .discord-embed .discord-embed-author .discord-author-image { 99 | border-radius: 50%; 100 | height: 24px; 101 | margin-right: 8px; 102 | width: 24px; 103 | } 104 | 105 | .discord-embed .discord-embed-provider { 106 | font-size: 0.75rem; 107 | line-height: 1rem; 108 | font-weight: 400; 109 | grid-column: 1/1; 110 | margin-top: 8px; 111 | unicode-bidi: plaintext; 112 | text-align: left; 113 | } 114 | 115 | .discord-light-theme .discord-embed .discord-embed-provider { 116 | color: #4f545c; 117 | } 118 | 119 | .discord-embed .discord-embed-title { 120 | -webkit-box-align: center; 121 | align-items: center; 122 | color: #fff; 123 | display: inline-block; 124 | font-size: 1rem; 125 | font-weight: 600; 126 | grid-column: 1 / 1; 127 | margin-top: 8px; 128 | min-width: 0; 129 | } 130 | 131 | .discord-embed .discord-embed-title a { 132 | color: #00aff4; 133 | font-weight: 600; 134 | } 135 | 136 | .discord-embed .discord-embed-image { 137 | border-radius: 4px; 138 | max-width: 100%; 139 | } 140 | 141 | .discord-embed .discord-embed-media { 142 | border-radius: 4px; 143 | contain: paint; 144 | display: block; 145 | grid-column: 1/1; 146 | margin-top: 16px; 147 | } 148 | 149 | .discord-embed .discord-embed-media.discord-embed-media-video { 150 | height: 225px; 151 | } 152 | 153 | .discord-embed .discord-embed.media .discord-embed-image { 154 | overflow: hidden; 155 | position: relative; 156 | user-select: text; 157 | } 158 | 159 | .discord-embed .discord-embed-media .discord-embed-video { 160 | -webkit-box-align: center; 161 | -webkit-box-pack: center; 162 | align-items: center; 163 | border-radius: 0; 164 | cursor: pointer; 165 | display: flex; 166 | height: 100%; 167 | justify-content: center; 168 | max-height: 100%; 169 | width: 100%; 170 | 171 | width: 400px; 172 | height: 225px; 173 | left: 0px; 174 | top: 0px; 175 | } 176 | 177 | .discord-embed-custom-emoji { 178 | display: inline-block; 179 | } 180 | 181 | .discord-embed-custom-emoji .discord-embed-custom-emoji-image { 182 | width: 18px; 183 | height: 18px; 184 | vertical-align: bottom; 185 | } 186 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-invite/discord-invite.css: -------------------------------------------------------------------------------- 1 | .discord-invite { 2 | background-color: #2f3136; 3 | border-radius: 4px; 4 | padding: 16px; 5 | width: 432px; 6 | } 7 | 8 | .discord-light-theme .discord-invite { 9 | background-color: #f2f3f5; 10 | } 11 | 12 | .discord-invite .discord-invite-header { 13 | font-weight: 700; 14 | font-size: 12px; 15 | line-height: 16px; 16 | margin-bottom: 12px; 17 | white-space: nowrap; 18 | text-overflow: ellipsis; 19 | overflow: hidden; 20 | text-transform: uppercase; 21 | color: #b9bbbe; 22 | } 23 | 24 | .discord-light-theme .discord-invite .discord-invite-header { 25 | color: #4f5660; 26 | } 27 | 28 | .discord-invite .discord-invite-root { 29 | display: flex; 30 | flex-flow: row nowrap; 31 | } 32 | 33 | .discord-invite .discord-invite-icon { 34 | background-color: #36393f; 35 | border-radius: 15px; 36 | margin-right: 16px; 37 | -webkit-box-flex: 0; 38 | -ms-flex: 0 0 auto; 39 | flex: 0 0 auto; 40 | width: 50px; 41 | height: 50px; 42 | } 43 | 44 | .discord-light-theme .discord-invite .discord-invite-icon { 45 | background-color: #fff; 46 | } 47 | 48 | .discord-invite .discord-invite-info { 49 | font-family: Whitney, 'Source Sans Pro', ui-sans-serif, system-ui, -apple-system, 'system-ui', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 50 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 51 | display: flex; 52 | flex: 1 1 auto; 53 | flex-direction: column; 54 | flex-wrap: nowrap; 55 | align-items: stretch; 56 | justify-content: center; 57 | } 58 | 59 | .discord-invite .discord-invite-title { 60 | white-space: nowrap; 61 | text-overflow: ellipsis; 62 | overflow: hidden; 63 | margin-bottom: 2px; 64 | color: white; 65 | font-size: 16px; 66 | line-height: 20px; 67 | font-weight: 700; 68 | display: flex; 69 | flex-direction: row; 70 | } 71 | 72 | .discord-light-theme .discord-invite .discord-invite-title { 73 | color: #060607; 74 | } 75 | 76 | .discord-invite .discord-invite-name { 77 | flex: 1 1 auto; 78 | overflow: hidden; 79 | text-overflow: ellipsis; 80 | white-space: nowrap; 81 | } 82 | 83 | .discord-invite .discord-invite-counts { 84 | display: flex; 85 | align-items: center; 86 | font-size: 14px; 87 | font-weight: 600; 88 | white-space: nowrap; 89 | text-overflow: ellipsis; 90 | overflow: hidden; 91 | color: #b9bbbe; 92 | line-height: 16px; 93 | } 94 | 95 | .discord-invite .discord-invite-status { 96 | display: block; 97 | margin-right: 4px; 98 | width: 8px; 99 | height: 8px; 100 | border-radius: 50%; 101 | background-color: #747f8d; 102 | } 103 | 104 | .discord-invite .discord-invite-status-online { 105 | background-color: #3ba55d; 106 | } 107 | 108 | .discord-invite .discord-invite-count { 109 | -webkit-box-flex: 0; 110 | -ms-flex: 0 1 auto; 111 | flex: 0 1 auto; 112 | margin-right: 8px; 113 | color: #b9bbbe; 114 | white-space: nowrap; 115 | text-overflow: ellipsis; 116 | overflow: hidden; 117 | } 118 | 119 | .discord-invite .discord-invite-join { 120 | display: flex; 121 | justify-content: center; 122 | align-items: center; 123 | height: 40px; 124 | padding: 0 20px; 125 | align-self: center; 126 | margin-left: 10px; 127 | -webkit-box-flex: 0; 128 | -ms-flex: 0 0 auto; 129 | flex: 0 0 auto; 130 | line-height: 20px; 131 | border-radius: 3px; 132 | font-size: 14px; 133 | font-weight: 600; 134 | color: white !important; 135 | background-color: #3ba55d; 136 | -webkit-transition: background-color 0.17s ease; 137 | transition: background-color 0.17s ease; 138 | } 139 | 140 | .discord-invite .discord-invite-join:hover { 141 | background-color: #2d7d46; 142 | text-decoration: none; 143 | } 144 | 145 | .discord-invite .discord-invite-badge { 146 | -webkit-box-flex: 0; 147 | -ms-flex: 0 0 auto; 148 | flex: 0 0 auto; 149 | margin-right: 8px; 150 | width: 16px; 151 | height: 16px; 152 | align-self: center; 153 | position: relative; 154 | } 155 | 156 | .discord-invite .discord-invite-badge-verified { 157 | color: #3ba55d; 158 | } 159 | 160 | .discord-invite .discord-invite-badge-partnered { 161 | color: #5865f2; 162 | } 163 | 164 | .discord-invite .discord-invite-badge-container { 165 | position: absolute; 166 | top: -0.05px; 167 | left: 0.05px; 168 | right: 0; 169 | bottom: 0; 170 | display: flex; 171 | align-items: center; 172 | justify-content: center; 173 | pointer-events: none; 174 | color: white; 175 | } 176 | 177 | .discord-light-theme .discord-invite .discord-invite-counts, 178 | .discord-light-theme .discord-invite .discord-invite-count { 179 | color: #4f5660; 180 | } 181 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /* auto-generated react proxies */ 4 | import { createReactComponent } from './react-component-lib'; 5 | 6 | import type { JSX } from '@derockdev/discord-components-core'; 7 | 8 | import { defineCustomElements } from '@derockdev/discord-components-core/loader'; 9 | 10 | defineCustomElements(); 11 | export const DiscordActionRow = /*@__PURE__*/ createReactComponent('discord-action-row'); 12 | export const DiscordAttachment = /*@__PURE__*/ createReactComponent('discord-attachment'); 13 | export const DiscordAttachments = /*@__PURE__*/ createReactComponent('discord-attachments'); 14 | export const DiscordBold = /*@__PURE__*/ createReactComponent('discord-bold'); 15 | export const DiscordButton = /*@__PURE__*/ createReactComponent('discord-button'); 16 | export const DiscordCodeBlock = /*@__PURE__*/ createReactComponent('discord-code-block'); 17 | export const DiscordCommand = /*@__PURE__*/ createReactComponent('discord-command'); 18 | export const DiscordCustomEmoji = /*@__PURE__*/ createReactComponent('discord-custom-emoji'); 19 | export const DiscordEmbed = /*@__PURE__*/ createReactComponent('discord-embed'); 20 | export const DiscordEmbedDescription = /*@__PURE__*/ createReactComponent( 21 | 'discord-embed-description' 22 | ); 23 | export const DiscordEmbedField = /*@__PURE__*/ createReactComponent('discord-embed-field'); 24 | export const DiscordEmbedFields = /*@__PURE__*/ createReactComponent('discord-embed-fields'); 25 | export const DiscordEmbedFooter = /*@__PURE__*/ createReactComponent('discord-embed-footer'); 26 | export const DiscordHeader = /*@__PURE__*/ createReactComponent('discord-header'); 27 | export const DiscordInlineCode = /*@__PURE__*/ createReactComponent('discord-inline-code'); 28 | export const DiscordInvite = /*@__PURE__*/ createReactComponent('discord-invite'); 29 | export const DiscordItalic = /*@__PURE__*/ createReactComponent('discord-italic'); 30 | export const DiscordMention = /*@__PURE__*/ createReactComponent('discord-mention'); 31 | export const DiscordMessage = /*@__PURE__*/ createReactComponent('discord-message'); 32 | export const DiscordMessages = /*@__PURE__*/ createReactComponent('discord-messages'); 33 | export const DiscordQuote = /*@__PURE__*/ createReactComponent('discord-quote'); 34 | export const DiscordReaction = /*@__PURE__*/ createReactComponent('discord-reaction'); 35 | export const DiscordReactions = /*@__PURE__*/ createReactComponent('discord-reactions'); 36 | export const DiscordReply = /*@__PURE__*/ createReactComponent('discord-reply'); 37 | export const DiscordSpoiler = /*@__PURE__*/ createReactComponent('discord-spoiler'); 38 | export const DiscordSystemMessage = /*@__PURE__*/ createReactComponent( 39 | 'discord-system-message' 40 | ); 41 | export const DiscordTenorVideo = /*@__PURE__*/ createReactComponent('discord-tenor-video'); 42 | export const DiscordThread = /*@__PURE__*/ createReactComponent('discord-thread'); 43 | export const DiscordThreadMessage = /*@__PURE__*/ createReactComponent( 44 | 'discord-thread-message' 45 | ); 46 | export const DiscordTime = /*@__PURE__*/ createReactComponent('discord-time'); 47 | export const DiscordUnderlined = /*@__PURE__*/ createReactComponent('discord-underlined'); 48 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@sapphire", "plugin:import/typescript"], 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | }, 9 | "jsx": true 10 | }, 11 | "plugins": ["import", "jsx-a11y", "react", "react-hooks"], 12 | "settings": { 13 | "react": { 14 | "version": "detect" 15 | } 16 | }, 17 | "rules": { 18 | "no-extra-label": "warn", 19 | "no-iterator": "warn", 20 | "no-label-var": "warn", 21 | "no-labels": [ 22 | "warn", 23 | { 24 | "allowLoop": true, 25 | "allowSwitch": true 26 | } 27 | ], 28 | "no-lone-blocks": "warn", 29 | "no-loop-func": "warn", 30 | "no-mixed-operators": [ 31 | "warn", 32 | { 33 | "groups": [ 34 | ["&", "|", "^", "~", "<<", ">>", ">>>"], 35 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="], 36 | ["&&", "||"], 37 | ["in", "instanceof"] 38 | ], 39 | "allowSamePrecedence": false 40 | } 41 | ], 42 | "no-multi-str": "warn", 43 | "rest-spread-spacing": ["warn", "never"], 44 | "no-restricted-properties": [ 45 | "error", 46 | { 47 | "object": "require", 48 | "property": "ensure", 49 | "message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting" 50 | }, 51 | { 52 | "object": "System", 53 | "property": "import", 54 | "message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting" 55 | } 56 | ], 57 | "getter-return": "warn", 58 | "import/first": "error", 59 | "import/no-amd": "error", 60 | "import/no-webpack-loader-syntax": "error", 61 | "react/forbid-foreign-prop-types": [ 62 | "warn", 63 | { 64 | "allowInPropTypes": true 65 | } 66 | ], 67 | "react/jsx-no-comment-textnodes": "warn", 68 | "react/jsx-no-duplicate-props": "warn", 69 | "react/jsx-no-target-blank": "warn", 70 | "react/jsx-no-undef": "error", 71 | "react/jsx-pascal-case": [ 72 | "warn", 73 | { 74 | "allowAllCaps": true, 75 | "ignore": [] 76 | } 77 | ], 78 | "react/jsx-uses-react": "warn", 79 | "react/jsx-uses-vars": "warn", 80 | "react/no-children-prop": "warn", 81 | "react/no-access-state-in-setstate": "warn", 82 | "react/no-danger": "error", 83 | "react/no-danger-with-children": "warn", 84 | "react/no-direct-mutation-state": "warn", 85 | "react/no-is-mounted": "warn", 86 | "react/no-this-in-sfc": "warn", 87 | "react/no-typos": "error", 88 | "react/react-in-jsx-scope": "error", 89 | "react/require-render-return": "error", 90 | "react/style-prop-object": "warn", 91 | "react/void-dom-elements-no-children": "warn", 92 | "jsx-a11y/accessible-emoji": "warn", 93 | "jsx-a11y/alt-text": "warn", 94 | "jsx-a11y/anchor-has-content": "warn", 95 | "jsx-a11y/anchor-is-valid": [ 96 | "warn", 97 | { 98 | "aspects": ["noHref", "invalidHref"] 99 | } 100 | ], 101 | "jsx-a11y/aria-activedescendant-has-tabindex": "warn", 102 | "jsx-a11y/aria-props": "warn", 103 | "jsx-a11y/aria-proptypes": "warn", 104 | "jsx-a11y/aria-role": [ 105 | "warn", 106 | { 107 | "ignoreNonDOM": true 108 | } 109 | ], 110 | "jsx-a11y/aria-unsupported-elements": "warn", 111 | "jsx-a11y/heading-has-content": "warn", 112 | "jsx-a11y/iframe-has-title": "warn", 113 | "jsx-a11y/img-redundant-alt": "warn", 114 | "jsx-a11y/no-access-key": "warn", 115 | "jsx-a11y/no-distracting-elements": "warn", 116 | "jsx-a11y/no-redundant-roles": "warn", 117 | "jsx-a11y/role-has-required-aria-props": "warn", 118 | "jsx-a11y/role-supports-aria-props": "warn", 119 | "jsx-a11y/scope": "warn", 120 | "react-hooks/exhaustive-deps": "warn", 121 | "react-hooks/rules-of-hooks": "error" 122 | }, 123 | "overrides": [ 124 | { 125 | "files": ["packages/core/**"], 126 | "rules": { 127 | "@typescript-eslint/ban-types": 0, 128 | "@typescript-eslint/no-unused-vars": 0, 129 | "react/react-in-jsx-scope": 0 130 | } 131 | }, 132 | { 133 | "files": ["packages/react/**"], 134 | "rules": { 135 | "@typescript-eslint/ban-types": 0, 136 | "@typescript-eslint/no-floating-promises": 0, 137 | "@typescript-eslint/unbound-method": 0, 138 | "no-eq-null": 0, 139 | "@typescript-eslint/explicit-member-accessibility": 0, 140 | "@typescript-eslint/member-ordering": 0, 141 | "@typescript-eslint/naming-convention": 0, 142 | "@typescript-eslint/no-empty-function": 0, 143 | "@typescript-eslint/no-explicit-any": 0, 144 | "@typescript-eslint/no-unused-vars": 0, 145 | "@typescript-eslint/no-useless-constructor": 0, 146 | "prefer-template": 0 147 | } 148 | } 149 | ] 150 | } 151 | -------------------------------------------------------------------------------- /packages/core/src/components/discord-reply/discord-reply.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, h, Host, Prop } from '@stencil/core'; 2 | import Fragment from '../../Fragment'; 3 | import { avatars, Profile, profiles } from '../../options'; 4 | import AttachmentReply from '../svgs/attachment-reply'; 5 | import CommandReply from '../svgs/command-reply'; 6 | import ReplyIcon from '../svgs/reply-icon'; 7 | import VerifiedTick from '../svgs/verified-tick'; 8 | 9 | @Component({ 10 | tag: 'discord-reply', 11 | styleUrl: 'discord-reply.css' 12 | }) 13 | export class DiscordReply implements ComponentInterface { 14 | /** 15 | * The DiscordReply element. 16 | */ 17 | @Element() 18 | public el: HTMLElement; 19 | 20 | /** 21 | * The id of the profile data to use. 22 | */ 23 | @Prop() 24 | public profile: string; 25 | 26 | /** 27 | * The message author's username. 28 | * @default 'User' 29 | */ 30 | @Prop() 31 | public author = 'User'; 32 | 33 | /** 34 | * The message author's avatar. Can be an avatar shortcut, relative path, or external link. 35 | */ 36 | @Prop() 37 | public avatar: string; 38 | 39 | /** 40 | * Whether the message author is a bot or not. 41 | * Only works if `server` is `false` or `undefined`. 42 | */ 43 | @Prop() 44 | public bot = false; 45 | 46 | /** 47 | * Whether the message author is a server crosspost webhook or not. 48 | * Only works if `bot` is `false` or `undefined`. 49 | */ 50 | @Prop() 51 | public server = false; 52 | 53 | /** 54 | * Whether the author is the original poster. 55 | */ 56 | @Prop() 57 | public op = false; 58 | 59 | /** 60 | * Whether the bot is verified or not. 61 | * Only works if `bot` is `true` 62 | */ 63 | @Prop() 64 | public verified = false; 65 | 66 | /** 67 | * Whether the message has been edited or not. 68 | */ 69 | @Prop() 70 | public edited = false; 71 | 72 | /** 73 | * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). 74 | */ 75 | @Prop() 76 | public roleColor: string; 77 | 78 | /** 79 | * Whether the referenced message is from a response of a slash command. 80 | */ 81 | @Prop() 82 | public command = false; 83 | 84 | /** 85 | * Whether the referenced message contains attachments. 86 | */ 87 | @Prop() 88 | public attachment = false; 89 | 90 | /** 91 | * Whether this reply pings the original message sender, prepending an "@" on the author's username. 92 | */ 93 | @Prop() 94 | public mentions = false; 95 | 96 | public render() { 97 | const parent: HTMLDiscordMessageElement = this.el.parentElement as HTMLDiscordMessageElement; 98 | 99 | if (parent.tagName.toLowerCase() !== 'discord-message') { 100 | throw new Error('All components must be direct children of .'); 101 | } 102 | 103 | const resolveAvatar = (avatar: string): string => avatars[avatar] ?? avatar ?? avatars.default; 104 | 105 | const defaultData: Profile = { 106 | author: this.author, 107 | bot: this.bot, 108 | verified: this.verified, 109 | op: this.op, 110 | server: this.server, 111 | roleColor: this.roleColor 112 | }; 113 | const profileData: Profile = Reflect.get(profiles, this.profile) ?? {}; 114 | const profile: Profile = { ...defaultData, ...profileData, ...{ avatar: resolveAvatar(profileData.avatar ?? this.avatar) } }; 115 | 116 | const messageParent: HTMLDiscordMessagesElement = parent.parentElement as HTMLDiscordMessagesElement; 117 | 118 | return ( 119 | 120 | {messageParent.compactMode ? ( 121 |
122 | 123 |
124 | ) : ( 125 | {profile.author} 126 | )} 127 | { 128 | 129 | {profile.bot && !profile.server && ( 130 | 131 | {profile.verified && } 132 | Bot 133 | 134 | )} 135 | {profile.server && !profile.bot && Server} 136 | {profile.op && OP} 137 | 138 | } 139 | 140 | {this.mentions && '@'} 141 | {profile.author} 142 | 143 |
144 | 145 | {this.edited ? (edited) : ''} 146 |
147 | {this.command ? ( 148 | 149 | ) : ( 150 | this.attachment && 151 | )} 152 |
153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /packages/react/src/react-component-lib/createOverlayComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { OverlayEventDetail } from './interfaces'; 5 | import { StencilReactForwardedRef, attachProps, dashToPascalCase, defineCustomElement, setRef } from './utils'; 6 | 7 | interface OverlayElement extends HTMLElement { 8 | present: () => Promise; 9 | dismiss: (data?: any, role?: string | undefined) => Promise; 10 | } 11 | 12 | export interface ReactOverlayProps { 13 | children?: React.ReactNode; 14 | isOpen: boolean; 15 | onDidDismiss?: (event: CustomEvent) => void; 16 | onDidPresent?: (event: CustomEvent) => void; 17 | onWillDismiss?: (event: CustomEvent) => void; 18 | onWillPresent?: (event: CustomEvent) => void; 19 | } 20 | 21 | export const createOverlayComponent = ( 22 | tagName: string, 23 | controller: { create: (options: any) => Promise }, 24 | customElement?: any 25 | ) => { 26 | defineCustomElement(tagName, customElement); 27 | 28 | const displayName = dashToPascalCase(tagName); 29 | const didDismissEventName = `on${displayName}DidDismiss`; 30 | const didPresentEventName = `on${displayName}DidPresent`; 31 | const willDismissEventName = `on${displayName}WillDismiss`; 32 | const willPresentEventName = `on${displayName}WillPresent`; 33 | 34 | type Props = OverlayComponent & 35 | ReactOverlayProps & { 36 | forwardedRef?: StencilReactForwardedRef; 37 | }; 38 | 39 | let isDismissing = false; 40 | 41 | class Overlay extends React.Component { 42 | overlay?: OverlayType; 43 | el!: HTMLDivElement; 44 | 45 | constructor(props: Props) { 46 | super(props); 47 | if (typeof document !== 'undefined') { 48 | this.el = document.createElement('div'); 49 | } 50 | this.handleDismiss = this.handleDismiss.bind(this); 51 | } 52 | 53 | static get displayName() { 54 | return displayName; 55 | } 56 | 57 | componentDidMount() { 58 | if (this.props.isOpen) { 59 | this.present(); 60 | } 61 | } 62 | 63 | componentWillUnmount() { 64 | if (this.overlay) { 65 | this.overlay.dismiss(); 66 | } 67 | } 68 | 69 | handleDismiss(event: CustomEvent>) { 70 | if (this.props.onDidDismiss) { 71 | this.props.onDidDismiss(event); 72 | } 73 | setRef(this.props.forwardedRef, null); 74 | } 75 | 76 | shouldComponentUpdate(nextProps: Props) { 77 | // Check if the overlay component is about to dismiss 78 | if (this.overlay && nextProps.isOpen !== this.props.isOpen && !nextProps.isOpen) { 79 | isDismissing = true; 80 | } 81 | 82 | return true; 83 | } 84 | 85 | async componentDidUpdate(prevProps: Props) { 86 | if (this.overlay) { 87 | attachProps(this.overlay, this.props, prevProps); 88 | } 89 | 90 | if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) { 91 | this.present(prevProps); 92 | } 93 | if (this.overlay && prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) { 94 | await this.overlay.dismiss(); 95 | isDismissing = false; 96 | 97 | /** 98 | * Now that the overlay is dismissed 99 | * we need to render again so that any 100 | * inner components will be unmounted 101 | */ 102 | this.forceUpdate(); 103 | } 104 | } 105 | 106 | async present(prevProps?: Props) { 107 | const { children, isOpen, onDidDismiss, onDidPresent, onWillDismiss, onWillPresent, ...cProps } = this.props; 108 | const elementProps = { 109 | ...cProps, 110 | ref: this.props.forwardedRef, 111 | [didDismissEventName]: this.handleDismiss, 112 | [didPresentEventName]: (e: CustomEvent) => this.props.onDidPresent && this.props.onDidPresent(e), 113 | [willDismissEventName]: (e: CustomEvent) => this.props.onWillDismiss && this.props.onWillDismiss(e), 114 | [willPresentEventName]: (e: CustomEvent) => this.props.onWillPresent && this.props.onWillPresent(e) 115 | }; 116 | 117 | this.overlay = await controller.create({ 118 | ...elementProps, 119 | component: this.el, 120 | componentProps: {} 121 | }); 122 | 123 | setRef(this.props.forwardedRef, this.overlay); 124 | attachProps(this.overlay, elementProps, prevProps); 125 | 126 | await this.overlay.present(); 127 | } 128 | 129 | render() { 130 | /** 131 | * Continue to render the component even when 132 | * overlay is dismissing otherwise component 133 | * will be hidden before animation is done. 134 | */ 135 | return ReactDOM.createPortal(this.props.isOpen || isDismissing ? this.props.children : null, this.el); 136 | } 137 | } 138 | 139 | return React.forwardRef((props, ref) => { 140 | return ; 141 | }); 142 | }; 143 | --------------------------------------------------------------------------------