34 Commits

Author SHA1 Message Date
061608323f codex: fix stupid react
Some checks failed
release / release (push) Has been cancelled
tests / build (push) Has been cancelled
tests / lint (push) Has been cancelled
tests / test (push) Has been cancelled
2026-01-02 20:53:54 +02:00
d1611d8f64 codex: components v2 maybe
Some checks failed
release / release (push) Has been cancelled
tests / build (push) Has been cancelled
tests / lint (push) Has been cancelled
tests / test (push) Has been cancelled
2026-01-02 20:18:16 +02:00
Darius
b641885112 Merge pull request #43 from domin-mnd/main 2023-11-01 15:51:44 -05:00
Domin-MND
2a8ee7885d revert readme 2023-11-01 22:01:36 +03:00
Domin-MND
6c71073d10 fix spacing 2023-10-31 20:14:57 +03:00
Domin-MND
5674e3c1b6 update readme & remove useless comments 2023-10-31 20:07:21 +03:00
Domin-MND
a41c825cdd use typescript for docs 2023-10-29 13:51:00 +03:00
Darius
a00fbc0631 Merge pull request #42 from itsMapleLeaf/changeset-release/main 2023-10-28 14:53:10 -05:00
github-actions[bot]
a713f17a5c Version Packages 2023-10-28 19:49:36 +00:00
Darius
44795cd7cc Merge pull request #41 from itsMapleLeaf/dev 2023-10-28 14:49:01 -05:00
itsMapleLeaf
17978a5252 node 16 is EOL 2023-10-28 14:47:01 -05:00
itsMapleLeaf
95fb342183 remove format and fix lint:prettier 2023-10-28 14:46:25 -05:00
itsMapleLeaf
0772ca4502 fix test command interaction 2023-10-28 14:45:03 -05:00
itsMapleLeaf
11153dfe0f breaking: more descriptive component event types 2023-10-28 14:39:16 -05:00
itsMapleLeaf
fb0a997855 changeset 2023-10-28 14:34:13 -05:00
itsMapleLeaf
da1c62f2f0 public interface tweaks and such 2023-10-28 14:34:09 -05:00
Darius
cdc22b7916 Merge pull request #40 from domin-mnd/main 2023-10-28 13:04:33 -05:00
Domin-MND
7fee69c8ae fix select-menu guide 2023-10-27 16:09:18 +03:00
Domin-MND
c2e5dc04dd fix api guides 2023-10-27 16:06:00 +03:00
Domin-MND
390da4cab6 remove initial content for create methods 2023-10-24 19:58:48 +03:00
Domin-MND
def0c46f13 fix monorepo formatting 2023-10-23 23:25:44 +03:00
Domin-MND
8b6e283810 update guides 2023-10-23 23:22:25 +03:00
Domin-MND
13fcf7ddc9 match test adapter syntax 2023-10-23 22:25:06 +03:00
Domin-MND
ce12351a24 fix formatting 2023-10-23 22:08:08 +03:00
Domin-MND
73bb098774 add options for component event 2023-10-23 22:05:05 +03:00
Domin-MND
4ee4d4ab91 add options for component event 2023-10-23 22:02:33 +03:00
Domin-MND
f998a0e09a fix djs manual test 2023-10-23 12:24:24 +03:00
Domin-MND
453192cc96 cleanup 2023-10-23 11:51:59 +03:00
Domin-MND
d387f669ab more descriptive djs adapter methods 2023-10-21 11:16:58 +03:00
Darius
9aec87ae9f Merge pull request #39 from domin-mnd/main 2023-10-19 13:05:01 -05:00
Domin-MND
65d1d68bb0 fix id raising 2023-10-19 16:37:51 +03:00
Domin-MND
dfb7562c97 use reply renderer for ephermalReply 2023-10-18 21:48:38 +03:00
Domin-MND
9e2be6c2e0 add opts argument support 2023-10-18 21:39:17 +03:00
Domin-MND
d078995516 deprecate ephemeralReply in adapters 2023-10-18 20:59:14 +03:00
66 changed files with 5509 additions and 7643 deletions

View File

@@ -1,42 +1,30 @@
<center>
<img src="packages/website/src/assets/banner.png" alt="Reacord: Create interactive Discord messages using React">
</center>
## Installation ∙ [![npm](https://img.shields.io/npm/v/reacord?color=blue&style=flat-square)](https://www.npmjs.com/package/reacord)
```console
# npm
npm install reacord react discord.js
# yarn
yarn add reacord react discord.js
# pnpm
pnpm add reacord react discord.js
# bun
bun add reacord react discord.js
```
## Get Started
[Visit the docs to get started.](https://reacord.mapleleaf.dev/guides/getting-started)
## Example
<!-- prettier-ignore -->
```tsx
import * as React from "react"
import { useState } from "react"
import { Embed, Button } from "reacord"
function Counter() {
const [count, setCount] = React.useState(0)
const [count, setCount] = useState(0)
return (
<>
<Embed title="Counter">
This button has been clicked {count} times.
</Embed>
<Button onClick={() => setCount(count + 1)}>
+1
</Button>
<Button
label="+1"
onClick={() => setCount(count + 1)}
/>
</>
)
}

1412
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,17 @@
{
"name": "reacord-monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"lint": "run-s --continue-on-error lint:*",
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
"lint:prettier": "prettier . --write --cache --list-different",
"lint:types": "tsc -b & pnpm -r --parallel run typecheck",
"astro-sync": "pnpm --filter website exec astro sync",
"format": "run-s --continue-on-error format:*",
"format:eslint": "eslint . --report-unused-disable-directives --fix",
"format:prettier": "prettier --cache --write . \"**/*.astro\"",
"lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different",
"lint:types": "bun run --cwd packages/helpers typecheck && bun run --cwd packages/reacord typecheck",
"test": "vitest",
"build": "pnpm -r run build",
"build:website": "pnpm --filter website... run build",
"start": "pnpm -C packages/website run start",
"start:website": "pnpm -C packages/website run start",
"release": "pnpm -r run build && changeset publish"
"build": "bun run --cwd packages/reacord build",
"release": "bun run build && changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.26.2",
@@ -24,7 +20,6 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.0.3",
"react": "^18.2.0",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
},
@@ -36,8 +31,7 @@
],
"ignorePatterns": [
"node_modules",
"dist",
"packages/website/public/api"
"dist"
],
"rules": {
"@typescript-eslint/no-non-null-assertion": "warn",

View File

@@ -1,5 +1,40 @@
# reacord
## 0.6.0
### Minor Changes
- 11153df: breaking: more descriptive component event types
- fb0a997: add new descriptive adapter methods
The reacord instance names have been updated, and the old names are now deprecated.
- `send` -> `createChannelMessage`
- `reply` -> `createInteractionReply`
These new methods also accept discord JS options. Usage example:
```ts
// can accept either a channel object or a channel ID
reacord.createChannelMessage(channel)
reacord.createChannelMessage(channel, {
tts: true,
})
reacord.createChannelMessage(channel, {
reply: {
messageReference: "123456789012345678",
failIfNotExists: false,
},
})
reacord.createInteractionReply(interaction)
reacord.createInteractionReply(interaction, {
ephemeral: true,
})
```
These new methods replace the old ones, which are now deprecated.
## 0.5.5
### Patch Changes

View File

@@ -9,41 +9,52 @@ export interface ComponentEvent {
*
* @see https://discord.com/developers/docs/resources/channel#message-object
*/
message: MessageInfo
message: ComponentEventMessage
/**
* The channel that this event occurred in.
*
* @see https://discord.com/developers/docs/resources/channel#channel-object
*/
channel: ChannelInfo
channel: ComponentEventChannel
/**
* The user that triggered this event.
*
* @see https://discord.com/developers/docs/resources/user#user-object
*/
user: UserInfo
user: ComponentEventUser
/**
* The guild that this event occurred in.
*
* @see https://discord.com/developers/docs/resources/guild#guild-object
*/
guild?: GuildInfo
guild?: ComponentEventGuild
/** Create a new reply to this event. */
reply(content?: ReactNode): ReacordInstance
reply(
content?: ReactNode,
options?: ComponentEventReplyOptions,
): ReacordInstance
/**
* Create an ephemeral reply to this event, shown only to the user who
* triggered it.
*
* @deprecated Use event.reply(content, { ephemeral: true })
*/
ephemeralReply(content?: ReactNode): ReacordInstance
}
/** @category Component Event */
export interface ChannelInfo {
export interface ComponentEventReplyOptions {
ephemeral?: boolean
tts?: boolean
}
/** @category Component Event */
export interface ComponentEventChannel {
id: string
name?: string
topic?: string
@@ -55,11 +66,11 @@ export interface ChannelInfo {
}
/** @category Component Event */
export interface MessageInfo {
export interface ComponentEventMessage {
id: string
channelId: string
authorId: string
member?: GuildMemberInfo
member?: ComponentEventGuildMember
content: string
timestamp: string
editedTimestamp?: string
@@ -70,14 +81,14 @@ export interface MessageInfo {
}
/** @category Component Event */
export interface GuildInfo {
export interface ComponentEventGuild {
id: string
name: string
member: GuildMemberInfo
member: ComponentEventGuildMember
}
/** @category Component Event */
export interface GuildMemberInfo {
export interface ComponentEventGuildMember {
id: string
nick?: string
displayName: string
@@ -92,7 +103,7 @@ export interface GuildMemberInfo {
}
/** @category Component Event */
export interface UserInfo {
export interface ComponentEventUser {
id: string
username: string
discriminator: string

View File

@@ -13,6 +13,13 @@ import type { ComponentEvent } from "../component-event"
import { OptionNode } from "./option-node"
import { omit } from "@reacord/helpers/omit.js"
export type SelectMenuType =
| "string"
| "user"
| "role"
| "mentionable"
| "channel"
/** @category Select */
export interface SelectProps {
children?: ReactNode
@@ -25,6 +32,20 @@ export interface SelectProps {
/** The text shown when no value is selected */
placeholder?: string
/**
* The kind of select menu to render.
*
* Defaults to `string`.
*/
menuType?: SelectMenuType
/**
* Limit the channel types shown in a channel select menu.
*
* This is only used when `menuType` is `channel`.
*/
channelTypes?: number[]
/** Set to true to allow multiple selected values */
multiple?: boolean

View File

@@ -1,10 +1,13 @@
import type { ReacordInstance } from "./instance.js"
import { raise } from "@reacord/helpers/raise.js"
import * as React from "react"
import type { MessageStore } from "../internal/message-store.js"
const Context = React.createContext<ReacordInstance | undefined>(undefined)
const MessageContext = React.createContext<MessageStore | undefined>(undefined)
export const InstanceProvider = Context.Provider
export const MessageProvider = MessageContext.Provider
/**
* Get the associated instance for the current component.
@@ -18,3 +21,27 @@ export function useInstance(): ReacordInstance {
raise("Could not find instance, was this component rendered via Reacord?")
)
}
/**
* Get the message that the current component is rendered into.
*
* @category Core
*/
export function useMessage() {
const store =
React.useContext(MessageContext) ??
raise("Could not find message store, was this component rendered via Reacord?")
const getSnapshot = React.useCallback(() => store.getSnapshot(), [store])
if (React.useSyncExternalStore) {
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
}
const [value, setValue] = React.useState(getSnapshot)
React.useEffect(() => store.subscribe(() => setValue(getSnapshot())), [
store,
getSnapshot,
])
return value
}

View File

@@ -7,7 +7,7 @@ import type { ReactNode } from "react"
*/
export interface ReacordInstance {
/** Render some JSX to this instance (edits the message) */
render: (content: ReactNode) => void
render: (content: ReactNode) => ReacordInstance
/** Remove this message */
destroy: () => void

View File

@@ -14,11 +14,12 @@ import type {
import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer"
import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer"
import type {
ChannelInfo,
GuildInfo,
GuildMemberInfo,
MessageInfo,
UserInfo,
ComponentEventChannel,
ComponentEventGuild,
ComponentEventGuildMember,
ComponentEventMessage,
ComponentEventReplyOptions,
ComponentEventUser,
} from "./component-event"
import type { ReacordInstance } from "./instance"
import type { ReacordConfig } from "./reacord"
@@ -37,7 +38,7 @@ export class ReacordDiscordJs extends Reacord {
super(config)
client.on("interactionCreate", (interaction) => {
if (interaction.isButton() || interaction.isStringSelectMenu()) {
if (interaction.isButton() || interaction.isAnySelectMenu()) {
this.handleComponentInteraction(
this.createReacordComponentInteraction(interaction),
)
@@ -48,14 +49,49 @@ export class ReacordDiscordJs extends Reacord {
/**
* Sends a message to a channel.
*
* @param target Discord channel object.
* @param [options] Options for the channel message
* @see https://reacord.mapleleaf.dev/guides/sending-messages
* @see {@link Discord.MessageCreateOptions}
*/
public createChannelMessage(
target: Discord.ChannelResolvable,
options: Discord.MessageCreateOptions = {},
): ReacordInstance {
return this.createInstance(
this.createChannelMessageRenderer(target, options),
)
}
/**
* Replies to a command interaction by sending a message.
*
* @param interaction Discord command interaction object.
* @param [options] Custom options for the interaction reply method.
* @see https://reacord.mapleleaf.dev/guides/sending-messages
* @see {@link Discord.InteractionReplyOptions}
*/
public createInteractionReply(
interaction: Discord.CommandInteraction,
options: Discord.InteractionReplyOptions = {},
): ReacordInstance {
return this.createInstance(
this.createInteractionReplyRenderer(interaction, options),
)
}
/**
* Sends a message to a channel.
*
* @deprecated Use reacord.createChannelMessage() instead.
* @see https://reacord.mapleleaf.dev/guides/sending-messages
*/
override send(
channelId: string,
public send(
channel: Discord.ChannelResolvable,
initialContent?: React.ReactNode,
): ReacordInstance {
return this.createInstance(
this.createChannelRenderer(channelId),
this.createChannelMessageRenderer(channel, {}),
initialContent,
)
}
@@ -63,14 +99,15 @@ export class ReacordDiscordJs extends Reacord {
/**
* Sends a message as a reply to a command interaction.
*
* @deprecated Use reacord.createInteractionReply() instead.
* @see https://reacord.mapleleaf.dev/guides/sending-messages
*/
override reply(
public reply(
interaction: Discord.CommandInteraction,
initialContent?: React.ReactNode,
): ReacordInstance {
return this.createInstance(
this.createInteractionReplyRenderer(interaction),
this.createInteractionReplyRenderer(interaction, {}),
initialContent,
)
}
@@ -78,31 +115,51 @@ export class ReacordDiscordJs extends Reacord {
/**
* Sends an ephemeral message as a reply to a command interaction.
*
* @deprecated Use reacord.createInteractionReply(interaction, { ephemeral:
* true })
* @see https://reacord.mapleleaf.dev/guides/sending-messages
*/
override ephemeralReply(
public ephemeralReply(
interaction: Discord.CommandInteraction,
initialContent?: React.ReactNode,
): ReacordInstance {
return this.createInstance(
this.createEphemeralInteractionReplyRenderer(interaction),
this.createInteractionReplyRenderer(interaction, {
ephemeral: true,
}),
initialContent,
)
}
private createChannelRenderer(channelId: string) {
private createChannelMessageRenderer(
channelResolvable: Discord.ChannelResolvable,
messageCreateOptions?: Discord.MessageCreateOptions,
) {
return new ChannelMessageRenderer({
send: async (options) => {
const channel =
this.client.channels.cache.get(channelId) ??
(await this.client.channels.fetch(channelId)) ??
raise(`Channel ${channelId} not found`)
if (!channel.isTextBased()) {
raise(`Channel ${channelId} is not a text channel`)
send: async (messageOptions) => {
let channel = this.client.channels.resolve(channelResolvable)
if (!channel && typeof channelResolvable === "string") {
channel = await this.client.channels.fetch(channelResolvable)
}
const message = await channel.send(getDiscordMessageOptions(options))
if (!channel) {
const id =
typeof channelResolvable === "string"
? channelResolvable
: channelResolvable.id
raise(`Channel ${id} not found`)
}
if (!channel.isTextBased()) {
raise(`Channel ${channel.id} must be a text channel`)
}
const textChannel = channel as Discord.TextBasedChannel &
Discord.PartialTextBasedChannelFields
const message = await textChannel.send({
...getDiscordMessageOptions(messageOptions),
...messageCreateOptions,
})
return createReacordMessage(message)
},
})
@@ -112,20 +169,22 @@ export class ReacordDiscordJs extends Reacord {
interaction:
| Discord.CommandInteraction
| Discord.MessageComponentInteraction,
interactionReplyOptions: Discord.InteractionReplyOptions,
) {
return new InteractionReplyRenderer({
type: "command",
id: interaction.id,
reply: async (options) => {
interactionId: interaction.id,
reply: async (messageOptions) => {
const message = await interaction.reply({
...getDiscordMessageOptions(options),
...getDiscordMessageOptions(messageOptions),
...interactionReplyOptions,
fetchReply: true,
})
return createReacordMessage(message)
},
followUp: async (options) => {
followUp: async (messageOptions) => {
const message = await interaction.followUp({
...getDiscordMessageOptions(options),
...getDiscordMessageOptions(messageOptions),
...interactionReplyOptions,
fetchReply: true,
})
return createReacordMessage(message)
@@ -133,36 +192,11 @@ export class ReacordDiscordJs extends Reacord {
})
}
private createEphemeralInteractionReplyRenderer(
interaction:
| Discord.CommandInteraction
| Discord.MessageComponentInteraction,
) {
return new InteractionReplyRenderer({
type: "command",
id: interaction.id,
reply: async (options) => {
await interaction.reply({
...getDiscordMessageOptions(options),
ephemeral: true,
})
return createEphemeralReacordMessage()
},
followUp: async (options) => {
await interaction.followUp({
...getDiscordMessageOptions(options),
ephemeral: true,
})
return createEphemeralReacordMessage()
},
})
}
private createReacordComponentInteraction(
interaction: Discord.MessageComponentInteraction,
): ComponentInteraction {
// todo please dear god clean this up
const channel: ChannelInfo = interaction.channel
const channel: ComponentEventChannel = interaction.channel
? {
...pruneNullishValues(
pick(interaction.channel, [
@@ -178,43 +212,18 @@ export class ReacordDiscordJs extends Reacord {
}
: raise("Non-channel interactions are not supported")
const message: MessageInfo =
const message: ComponentEventMessage =
interaction.message instanceof Discord.Message
? {
...pick(interaction.message, [
"id",
"channelId",
"authorId",
"content",
"tts",
"mentionEveryone",
]),
timestamp: new Date(
interaction.message.createdTimestamp,
).toISOString(),
editedTimestamp: interaction.message.editedTimestamp
? new Date(interaction.message.editedTimestamp).toISOString()
: undefined,
mentions: interaction.message.mentions.users.map((u) => u.id),
authorId: interaction.message.author.id,
mentionEveryone: interaction.message.mentions.everyone,
}
? createComponentEventMessage(interaction.message)
: raise("Message not found")
const member: GuildMemberInfo | undefined =
const member: ComponentEventGuildMember | undefined =
interaction.member instanceof Discord.GuildMember
? {
...pruneNullishValues(
pick(interaction.member, [
"id",
"nick",
"displayName",
"avatarUrl",
"displayAvatarUrl",
"color",
"pending",
]),
pick(interaction.member, ["nick", "avatarUrl", "pending"]),
),
id: interaction.member.id,
displayName: interaction.member.displayName,
roles: interaction.member.roles.cache.map((role) => role.id),
joinedAt: interaction.member.joinedAt?.toISOString(),
@@ -226,17 +235,19 @@ export class ReacordDiscordJs extends Reacord {
}
: undefined
const guild: GuildInfo | undefined = interaction.guild
const guild: ComponentEventGuild | undefined = interaction.guild
? {
...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
id: interaction.guild.id,
name: interaction.guild.name,
member: member ?? raise("unexpected: member is undefined"),
}
: undefined
const user: UserInfo = {
...pruneNullishValues(
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
),
const user: ComponentEventUser = {
id: interaction.user.id,
username: interaction.user.username,
discriminator: interaction.user.discriminator,
tag: interaction.user.tag,
avatarUrl: interaction.user.avatarURL(),
accentColor: interaction.user.accentColor ?? undefined,
}
@@ -275,15 +286,18 @@ export class ReacordDiscordJs extends Reacord {
user,
guild,
reply: (content?: ReactNode) =>
reply: (content?: ReactNode, options?: ComponentEventReplyOptions) =>
this.createInstance(
this.createInteractionReplyRenderer(interaction),
this.createInteractionReplyRenderer(interaction, options ?? {}),
content,
),
/** @deprecated Use event.reply(content, { ephemeral: true }) */
ephemeralReply: (content: ReactNode) =>
this.createInstance(
this.createEphemeralInteractionReplyRenderer(interaction),
this.createInteractionReplyRenderer(interaction, {
ephemeral: true,
}),
content,
),
},
@@ -296,7 +310,7 @@ export class ReacordDiscordJs extends Reacord {
}
}
if (interaction.isStringSelectMenu()) {
if (interaction.isAnySelectMenu()) {
return {
...baseProps,
type: "select",
@@ -313,6 +327,7 @@ export class ReacordDiscordJs extends Reacord {
function createReacordMessage(message: Discord.Message): Message {
return {
data: createComponentEventMessage(message),
edit: async (options) => {
await message.edit(getDiscordMessageOptions(options))
},
@@ -322,16 +337,25 @@ function createReacordMessage(message: Discord.Message): Message {
}
}
function createEphemeralReacordMessage(): Message {
function createComponentEventMessage(
message: Discord.Message,
): ComponentEventMessage {
return {
edit: () => {
console.warn("Ephemeral messages can't be edited")
return Promise.resolve()
},
delete: () => {
console.warn("Ephemeral messages can't be deleted")
return Promise.resolve()
},
...pick(message, [
"id",
"channelId",
"authorId",
"content",
"tts",
"mentionEveryone",
]),
timestamp: new Date(message.createdTimestamp).toISOString(),
editedTimestamp: message.editedTimestamp
? new Date(message.editedTimestamp).toISOString()
: undefined,
mentions: message.mentions.users.map((u) => u.id),
authorId: message.author.id,
mentionEveryone: message.mentions.everyone,
}
}
@@ -381,14 +405,58 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
// future proofing
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (component.type === "select") {
return {
...component,
type: Discord.ComponentType.SelectMenu,
options: component.options.map((option) => ({
...option,
default: component.values?.includes(option.value),
})),
const {
menuType,
values,
options: selectOptions,
channelTypes,
multiple,
...rest
} = component
if (menuType === "string" || menuType == undefined) {
return {
...rest,
type: Discord.ComponentType.StringSelect,
options: selectOptions.map((option) => ({
...option,
default: values?.includes(option.value),
})),
}
}
if (menuType === "user") {
return {
...rest,
type: Discord.ComponentType.UserSelect,
}
}
if (menuType === "role") {
return {
...rest,
type: Discord.ComponentType.RoleSelect,
}
}
if (menuType === "mentionable") {
return {
...rest,
type: Discord.ComponentType.MentionableSelect,
}
}
if (menuType === "channel") {
return {
...rest,
type: Discord.ComponentType.ChannelSelect,
channelTypes,
}
}
raise(
`Unsupported select menu type: ${menuType ?? "string"}`,
)
}
component satisfies never

View File

@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
import type { ComponentInteraction } from "../internal/interaction.js"
import { reconciler } from "../internal/reconciler.js"
import type { Renderer } from "../internal/renderers/renderer.js"
import { InstanceProvider } from "./instance-context.js"
import { InstanceProvider, MessageProvider } from "./instance-context.js"
import type { ReacordInstance } from "./instance.js"
/** @category Core */
@@ -23,10 +23,6 @@ export abstract class Reacord {
constructor(private readonly config: ReacordConfig = {}) {}
abstract send(...args: unknown[]): ReacordInstance
abstract reply(...args: unknown[]): ReacordInstance
abstract ephemeralReply(...args: unknown[]): ReacordInstance
protected handleComponentInteraction(interaction: ComponentInteraction) {
for (const renderer of this.renderers) {
if (renderer.handleComponentInteraction(interaction)) return
@@ -58,9 +54,14 @@ export abstract class Reacord {
const instance: ReacordInstance = {
render: (content: ReactNode) => {
reconciler.updateContainer(
<InstanceProvider value={instance}>{content}</InstanceProvider>,
<InstanceProvider value={instance}>
<MessageProvider value={renderer.messageStore}>
{content}
</MessageProvider>
</InstanceProvider>,
container,
)
return instance
},
deactivate: () => {
this.deactivate(renderer)

View File

@@ -0,0 +1,22 @@
import type { ComponentEventMessage } from "../core/component-event"
export class MessageStore {
private value: ComponentEventMessage | undefined
private listeners = new Set<() => void>()
getSnapshot = () => this.value
subscribe = (listener: () => void) => {
this.listeners.add(listener)
return () => {
this.listeners.delete(listener)
}
}
set(value: ComponentEventMessage | undefined) {
this.value = value
for (const listener of this.listeners) {
listener()
}
}
}

View File

@@ -1,3 +1,4 @@
import type { ComponentEventMessage } from "../core/component-event"
import type { EmbedOptions } from "../core/components/embed-options"
import type { SelectProps } from "../core/components/select"
import { last } from "@reacord/helpers/last"
@@ -47,6 +48,7 @@ export interface MessageSelectOptionOptions {
}
export interface Message {
data?: ComponentEventMessage
edit(options: MessageOptions): Promise<void>
delete(): Promise<void>
}

View File

@@ -15,7 +15,7 @@ const config: HostConfig<
never, // SuspenseInstance,
never, // HydratableInstance,
never, // PublicInstance,
never, // HostContext,
null, // HostContext,
true, // UpdatePayload,
never, // ChildSet,
number, // TimeoutHandle,

View File

@@ -1,4 +1,3 @@
import type { Interaction } from "../interaction"
import type { Message, MessageOptions } from "../message"
import { Renderer } from "./renderer"
@@ -6,17 +5,23 @@ import { Renderer } from "./renderer"
// so we know whether to call reply() or followUp()
const repliedInteractionIds = new Set<string>()
export type InteractionReplyRendererImplementation = {
interactionId: string
reply: (options: MessageOptions) => Promise<Message>
followUp: (options: MessageOptions) => Promise<Message>
}
export class InteractionReplyRenderer extends Renderer {
constructor(private interaction: Interaction) {
constructor(private implementation: InteractionReplyRendererImplementation) {
super()
}
protected createMessage(options: MessageOptions): Promise<Message> {
if (repliedInteractionIds.has(this.interaction.id)) {
return this.interaction.followUp(options)
if (repliedInteractionIds.has(this.implementation.interactionId)) {
return this.implementation.followUp(options)
}
repliedInteractionIds.add(this.interaction.id)
return this.interaction.reply(options)
repliedInteractionIds.add(this.implementation.interactionId)
return this.implementation.reply(options)
}
}

View File

@@ -1,5 +1,6 @@
import { Container } from "../container.js"
import type { ComponentInteraction } from "../interaction"
import { MessageStore } from "../message-store.js"
import type { Message, MessageOptions } from "../message"
import type { Node } from "../node.js"
import { Subject } from "rxjs"
@@ -12,6 +13,7 @@ type UpdatePayload =
export abstract class Renderer {
readonly nodes = new Container<Node<unknown>>()
readonly messageStore = new MessageStore()
private componentInteraction?: ComponentInteraction
private message?: Message
private active = true
@@ -75,6 +77,7 @@ export abstract class Renderer {
private async updateMessage(payload: UpdatePayload) {
if (payload.action === "destroy") {
this.updateSubscription.unsubscribe()
this.messageStore.set(undefined)
await this.message?.delete()
return
}
@@ -113,5 +116,6 @@ export abstract class Renderer {
}
this.message = await this.createMessage(payload.options)
this.messageStore.set(this.message.data)
}
}

View File

@@ -13,6 +13,6 @@ export * from "./core/components/link"
export * from "./core/components/option"
export * from "./core/components/select"
export * from "./core/instance"
export { useInstance } from "./core/instance-context"
export { useInstance, useMessage } from "./core/instance-context"
export * from "./core/reacord"
export * from "./core/reacord-discord-js"

View File

@@ -2,7 +2,7 @@
"name": "reacord",
"type": "module",
"description": "Create interactive Discord messages using React.",
"version": "0.5.5",
"version": "0.6.0",
"homepage": "https://reacord.mapleleaf.dev",
"repository": "https://github.com/itsMapleLeaf/reacord.git",
"changelog": "https://github.com/itsMapleLeaf/reacord/releases",
@@ -36,22 +36,21 @@
}
},
"scripts": {
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --sourcemap --dts --dts-resolve",
"build-watch": "pnpm build -- --watch",
"build": "rm -rf dist && mkdir -p dist && cp ../../README.md ../../LICENSE . && bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --external react --external react/jsx-runtime --external react/jsx-dev-runtime && bun build library/main.ts --target=node --outdir=dist --format=cjs --sourcemap=external --entry-naming=main.cjs --external react --external react/jsx-runtime --external react/jsx-dev-runtime && tsc -p tsconfig.build.json",
"build-watch": "bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --watch --external react --external react/jsx-runtime --external react/jsx-dev-runtime",
"test": "vitest --coverage --no-watch",
"test-dev": "vitest",
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
"typecheck": "tsc -b"
},
"dependencies": {
"@types/node": "^20.8.4",
"@types/react": "^18.2.27",
"@types/react-reconciler": "^0.28.5",
"react-reconciler": "^0.29.0",
"rxjs": "^7.8.1"
},
"peerDependencies": {
"discord.js": "^14",
"discord.js": "^14.25.1",
"react": ">=17"
},
"peerDependenciesMeta": {
@@ -63,15 +62,13 @@
"@reacord/helpers": "workspace:*",
"@types/lodash-es": "^4.17.9",
"c8": "^8.0.1",
"cpy-cli": "^5.0.0",
"discord.js": "^14.13.0",
"discord.js": "^14.25.1",
"dotenv": "^16.3.1",
"lodash-es": "^4.17.21",
"nodemon": "^3.0.1",
"prettier": "^3.0.3",
"pretty-ms": "^8.0.0",
"react": "^18.2.0",
"tsup": "^7.2.0",
"tsx": "^3.13.0",
"type-fest": "^4.4.0"
},

View File

@@ -1,6 +1,8 @@
import { raise } from "@reacord/helpers/raise.js"
import {
Button,
Embed,
EmbedField,
Link,
Option,
ReacordDiscordJs,
@@ -11,7 +13,6 @@ import type { TextChannel } from "discord.js"
import { ChannelType, Client, IntentsBitField } from "discord.js"
import "dotenv/config"
import { kebabCase } from "lodash-es"
import * as React from "react"
import { useState } from "react"
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
@@ -50,12 +51,60 @@ const createTest = async (
}
await createTest("basic", (channel) => {
reacord.send(channel.id, "Hello, world!")
reacord.createChannelMessage(channel).render("Hello, world!")
})
await createTest("readme counter", (channel) => {
interface EmbedCounterProps {
count: number
visible: boolean
}
function EmbedCounter({ count, visible }: EmbedCounterProps) {
if (!visible) return <></>
return (
<Embed title="the counter">
<EmbedField name="is it even?">{count % 2 ? "no" : "yes"}</EmbedField>
</Embed>
)
}
function Counter() {
const [showEmbed, setShowEmbed] = useState(false)
const [count, setCount] = useState(0)
const instance = useInstance()
return (
<>
this button was clicked {count} times
<EmbedCounter count={count} visible={showEmbed} />
<Button
style="primary"
label="clicc"
onClick={() => setCount(count + 1)}
/>
<Button
style="secondary"
label={showEmbed ? "hide embed" : "show embed"}
onClick={() => setShowEmbed(!showEmbed)}
/>
<Button
style="danger"
label="deactivate"
onClick={() => instance.destroy()}
/>
</>
)
}
reacord.createChannelMessage(channel).render(<Counter />)
})
await createTest("counter", (channel) => {
const Counter = () => {
const [count, setCount] = React.useState(0)
function Counter() {
const [count, setCount] = useState(0)
return (
<>
count: {count}
@@ -73,7 +122,7 @@ await createTest("counter", (channel) => {
</>
)
}
reacord.send(channel.id, <Counter />)
reacord.createChannelMessage(channel).render(<Counter />)
})
await createTest("select", (channel) => {
@@ -102,8 +151,7 @@ await createTest("select", (channel) => {
)
}
const instance = reacord.send(
channel.id,
const instance = reacord.createChannelMessage(channel).render(
<FruitSelect
onConfirm={(value) => {
instance.render(`you chose ${value}`)
@@ -114,8 +162,7 @@ await createTest("select", (channel) => {
})
await createTest("ephemeral button", (channel) => {
reacord.send(
channel.id,
reacord.createChannelMessage(channel).render(
<>
<Button
label="public clic"
@@ -125,7 +172,7 @@ await createTest("ephemeral button", (channel) => {
/>
<Button
label="clic"
onClick={(event) => event.ephemeralReply("you clic")}
onClick={(event) => event.reply("you clic", { ephemeral: true })}
/>
</>,
)
@@ -136,9 +183,11 @@ await createTest("delete this", (channel) => {
const instance = useInstance()
return <Button label="delete this" onClick={() => instance.destroy()} />
}
reacord.send(channel.id, <DeleteThis />)
reacord.createChannelMessage(channel).render(<DeleteThis />)
})
await createTest("link", (channel) => {
reacord.send(channel.id, <Link label="hi" url="https://mapleleaf.dev" />)
reacord
.createChannelMessage(channel)
.render(<Link label="hi" url="https://mapleleaf.dev" />)
})

View File

@@ -1,9 +1,12 @@
import { spawnSync } from "node:child_process"
import { fileURLToPath } from "node:url"
import { dirname } from "node:path"
import { createRequire } from "node:module"
import { beforeAll, expect, test } from "vitest"
beforeAll(() => {
spawnSync("pnpm", ["run", "build"])
const cwd = dirname(dirname(fileURLToPath(import.meta.url)))
spawnSync("bun", ["run", "build"], { cwd })
})
test("can require commonjs", () => {

View File

@@ -6,8 +6,9 @@ import { test } from "vitest"
test("rendering behavior", async () => {
const tester = new ReacordTester()
const reply = tester.reply()
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
const reply = tester
.createInteractionReply()
.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
await tester.assertMessages([
{
@@ -244,8 +245,7 @@ test("rendering behavior", async () => {
test("delete", async () => {
const tester = new ReacordTester()
const reply = tester.reply()
reply.render(
const reply = tester.createInteractionReply().render(
<>
some text
<Embed>some embed</Embed>

View File

@@ -53,9 +53,7 @@ test("single select", async () => {
])
}
const reply = tester.reply()
reply.render(<TestSelect />)
tester.createInteractionReply().render(<TestSelect />)
await assertSelect([])
expect(onSelect).toHaveBeenCalledTimes(0)
@@ -119,9 +117,7 @@ test("multiple select", async () => {
])
}
const reply = tester.reply()
reply.render(<TestSelect />)
tester.createInteractionReply().render(<TestSelect />)
await assertSelect([])
expect(onSelect).toHaveBeenCalledTimes(0)
@@ -148,7 +144,7 @@ test("multiple select", async () => {
test("optional onSelect + unknown value", async () => {
const tester = new ReacordTester()
tester.reply().render(<Select placeholder="select" />)
tester.createInteractionReply().render(<Select placeholder="select" />)
await tester.findSelectByPlaceholder("select").select("something")
await tester.assertMessages([
{

View File

@@ -8,10 +8,11 @@ import { setTimeout } from "node:timers/promises"
import type { ReactNode } from "react"
import { expect } from "vitest"
import type {
ChannelInfo,
GuildInfo,
MessageInfo,
UserInfo,
ComponentEventChannel,
ComponentEventGuild,
ComponentEventMessage,
ComponentEventReplyOptions,
ComponentEventUser,
} from "../library/core/component-event"
import type { ButtonClickEvent } from "../library/core/components/button"
import type { SelectChangeEvent } from "../library/core/components/select"
@@ -21,12 +22,14 @@ import type { Channel } from "../library/internal/channel"
import { Container } from "../library/internal/container"
import type {
ButtonInteraction,
CommandInteraction,
SelectInteraction,
} from "../library/internal/interaction"
import type { Message, MessageOptions } from "../library/internal/message"
import { ChannelMessageRenderer } from "../library/internal/renderers/channel-message-renderer"
import { InteractionReplyRenderer } from "../library/internal/renderers/interaction-reply-renderer"
import {
InteractionReplyRenderer,
type InteractionReplyRendererImplementation,
} from "../library/internal/renderers/interaction-reply-renderer"
export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0]
@@ -42,26 +45,28 @@ export class ReacordTester extends Reacord {
return [...this.messageContainer]
}
override send(initialContent?: ReactNode): ReacordInstance {
public createChannelMessage(): ReacordInstance {
return this.createInstance(
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
initialContent,
)
}
override reply(initialContent?: ReactNode): ReacordInstance {
public createMessageReply(): ReacordInstance {
return this.createInstance(
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
)
}
public createInteractionReply(
_options?: ComponentEventReplyOptions,
): ReacordInstance {
return this.createInstance(
new InteractionReplyRenderer(
new TestCommandInteraction(this.messageContainer),
),
initialContent,
)
}
override ephemeralReply(initialContent?: ReactNode): ReacordInstance {
return this.reply(initialContent)
}
assertMessages(expected: MessageSample[]) {
return waitFor(() => {
expect(this.sampleMessages()).toEqual(expected)
@@ -69,7 +74,7 @@ export class ReacordTester extends Reacord {
}
async assertRender(content: ReactNode, expected: MessageSample[]) {
const instance = this.reply()
const instance = this.createInteractionReply()
instance.render(content)
await this.assertMessages(expected)
instance.destroy()
@@ -171,9 +176,8 @@ class TestMessage implements Message {
}
}
class TestCommandInteraction implements CommandInteraction {
readonly type = "command"
readonly id = "test-command-interaction"
class TestCommandInteraction implements InteractionReplyRendererImplementation {
readonly interactionId = "test-command-interaction"
readonly channelId = "test-channel-id"
constructor(private messageContainer: Container<TestMessage>) {}
@@ -248,17 +252,19 @@ class TestSelectInteraction
class TestComponentEvent {
constructor(private tester: ReacordTester) {}
message: MessageInfo = {} as MessageInfo // todo
channel: ChannelInfo = {} as ChannelInfo // todo
user: UserInfo = {} as UserInfo // todo
guild: GuildInfo = {} as GuildInfo // todo
message: ComponentEventMessage = {} as ComponentEventMessage // todo
channel: ComponentEventChannel = {} as ComponentEventChannel // todo
user: ComponentEventUser = {} as ComponentEventUser // todo
guild: ComponentEventGuild = {} as ComponentEventGuild // todo
reply(content?: ReactNode): ReacordInstance {
return this.tester.reply(content)
return this.tester.createInteractionReply().render(content)
}
ephemeralReply(content?: ReactNode): ReacordInstance {
return this.tester.ephemeralReply(content)
return this.tester
.createInteractionReply({ ephemeral: true })
.render(content)
}
}

View File

@@ -49,7 +49,9 @@ describe("useInstance", () => {
}
const tester = new ReacordTester()
const instance = tester.send(<TestComponent name="parent" />)
const instance = tester
.createChannelMessage()
.render(<TestComponent name="parent" />)
await tester.assertMessages([messageOutput("parent")])
expect(instanceFromHook).toBe(instance)

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"declarationMap": true,
"outDir": "dist"
}
}

View File

@@ -1,7 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx"
"jsx": "react-jsx",
"skipLibCheck": true
},
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,11 +0,0 @@
node_modules
/.cache
/build
/public/build
.env
/public/api
cypress/videos
cypress/screenshots
*.out.css
/api
.astro

View File

@@ -1,48 +0,0 @@
# website
## 0.4.6
### Patch Changes
- Updated dependencies [ced48a3]
- reacord@0.5.5
## 0.4.5
### Patch Changes
- Updated dependencies [41c87e3]
- reacord@0.5.4
## 0.4.4
### Patch Changes
- Updated dependencies [104b175]
- Updated dependencies [156cf90]
- Updated dependencies [0bab505]
- Updated dependencies [d76f316]
- reacord@0.5.3
## 0.4.3
### Patch Changes
- Updated dependencies [9813a01]
- reacord@0.5.2
## 0.4.2
### Patch Changes
- Updated dependencies [72f4a4a]
- Updated dependencies [7536bde]
- Updated dependencies [e335165]
- reacord@0.5.1
## 0.4.1
### Patch Changes
- Updated dependencies [aa65da5]
- reacord@0.5.0

View File

@@ -1,20 +0,0 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import prefetch from "@astrojs/prefetch"
import react from "@astrojs/react"
import tailwind from "@astrojs/tailwind"
import { defineConfig } from "astro/config"
// https://astro.build/config
export default defineConfig({
integrations: [
tailwind({
applyBaseStyles: false,
}),
react(),
prefetch(),
],
markdown: {
shikiConfig: {},
},
})

View File

@@ -1,41 +0,0 @@
{
"type": "module",
"name": "website",
"version": "0.4.6",
"private": true,
"sideEffects": false,
"scripts": {
"dev": "run-p --race --print-label dev:*",
"dev:typedoc": "typedoc --watch",
"dev:astro": "astro dev",
"start": "astro preview",
"build": "typedoc && astro build",
"typecheck": "astro check && tsc -b"
},
"dependencies": {
"@astrojs/prefetch": "^0.3.0",
"@astrojs/react": "^2.3.2",
"@fontsource/jetbrains-mono": "^4.5.12",
"@fontsource/rubik": "^4.5.14",
"@heroicons/react": "^2.0.18",
"@reacord/helpers": "workspace:^",
"@tailwindcss/typography": "^0.5.10",
"astro": "^2.10.15",
"clsx": "^2.0.0",
"reacord": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^1.14.0"
},
"devDependencies": {
"@astrojs/tailwind": "^4.0.0",
"@total-typescript/ts-reset": "^0.5.1",
"@types/node": "^20.8.4",
"@types/react": "^18.2.27",
"@types/react-dom": "^18.2.12",
"npm-run-all": "^4.1.5",
"tailwindcss": "^3.3.3",
"typedoc": "^0.25.2",
"wait-on": "^7.0.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,3 +0,0 @@
<svg width="53" height="35" viewBox="0 0 53 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="3" cy="3" r="1" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 B

View File

@@ -1,22 +0,0 @@
---
import { HeartIcon } from "@heroicons/react/20/solid"
import { twMerge } from "tailwind-merge"
import ExternalLink from "./external-link.astro"
export interface Props {
class?: string
}
---
<footer class={twMerge("text-xs opacity-75", Astro.props.class)}>
<address class="not-italic">
&copy; {new Date().getFullYear()}
<ExternalLink class="link" href="https://github.com/itsMapleLeaf">
itsMapleLeaf
</ExternalLink>
</address>
<p>
Coded with <HeartIcon className="inline w-4 align-sub" /> using{" "}
<ExternalLink class="link" href="https://astro.build">Astro</ExternalLink>
</p>
</footer>

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
---
export type Props = astroHTML.JSX.AnchorHTMLAttributes
---
<a rel="noopener noreferrer" target="_blank" {...Astro.props}>
<slot />
</a>

View File

@@ -1,38 +0,0 @@
---
import { getCollection } from "astro:content"
import Layout from "./layout.astro"
import MainNavigation from "./main-navigation.astro"
const guides = await getCollection("guides")
---
<Layout>
<div class="isolate">
<header
class="bg-slate-700/30 shadow sticky top-0 backdrop-blur-sm transition z-10 flex"
>
<div class="container">
<MainNavigation />
</div>
</header>
<main class="container mt-8 flex items-start gap-4">
<nav class="w-48 sticky top-24 hidden md:block">
<h2 class="text-2xl">Guides</h2>
<ul class="mt-3 flex flex-col gap-2 items-start">
{
guides.map((guide) => (
<li>
<a class="link" href={`/guides/${guide.slug}`}>
{guide.data.title}
</a>
</li>
))
}
</ul>
</nav>
<section class="prose prose-invert pb-8 flex-1 min-w-0">
<slot />
</section>
</main>
</div>
</Layout>

View File

@@ -1,201 +0,0 @@
import clsx from "clsx"
import { useEffect, useRef, useState } from "react"
import blobComfyUrl from "~/assets/blob-comfy.png"
import cursorIbeamUrl from "~/assets/cursor-ibeam.png"
import cursorUrl from "~/assets/cursor.png"
import { raise } from "@reacord/helpers/raise.ts"
const defaultState = {
chatInputText: "",
chatInputCursorVisible: true,
messageVisible: false,
count: 0,
cursorLeft: "25%",
cursorBottom: "-15px",
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const animationFrame = () =>
new Promise((resolve) => requestAnimationFrame(resolve))
export function LandingAnimation() {
const [state, setState] = useState(defaultState)
const chatInputRef = useRef<HTMLDivElement>(null)
const addRef = useRef<HTMLDivElement>(null)
const deleteRef = useRef<HTMLDivElement>(null)
const cursorRef = useRef<HTMLImageElement>(null)
useEffect(() => {
const animateClick = (element: HTMLElement) =>
element.animate(
[{ transform: `translateY(2px)` }, { transform: `translateY(0px)` }],
300,
)
let running = true
void (async () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (running) {
setState(defaultState)
await delay(700)
for (const letter of "/counter") {
setState((state) => ({
...state,
chatInputText: state.chatInputText + letter,
}))
await delay(100)
}
await delay(1000)
setState((state) => ({
...state,
messageVisible: true,
chatInputText: "",
}))
await delay(1000)
setState((state) => ({
...state,
cursorLeft: "70px",
cursorBottom: "40px",
}))
await delay(1500)
for (let i = 0; i < 3; i++) {
setState((state) => ({
...state,
count: state.count + 1,
chatInputCursorVisible: false,
}))
animateClick(addRef.current ?? raise("addRef is null"))
await delay(700)
}
await delay(500)
setState((state) => ({
...state,
cursorLeft: "140px",
}))
await delay(1000)
animateClick(deleteRef.current ?? raise("deleteRef is null"))
setState((state) => ({ ...state, messageVisible: false }))
await delay(1000)
setState(() => ({
...defaultState,
chatInputCursorVisible: false,
}))
await delay(500)
}
})()
return () => {
running = false
}
}, [])
useEffect(() => {
let running = true
void (async () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (running) {
const cursor = cursorRef.current ?? raise("cursorRef is null")
const chatInput = chatInputRef.current ?? raise("chatInputRef is null")
// check if the cursor is in the input
const cursorRect = cursor.getBoundingClientRect()
const chatInputRect = chatInput.getBoundingClientRect()
const isOverInput =
cursorRef.current &&
chatInputRef.current &&
cursorRect.top + cursorRect.height / 2 > chatInputRect.top
cursor.src = isOverInput ? cursorIbeamUrl : cursorUrl
await animationFrame()
}
})()
return () => {
running = false
}
})
return (
<div
className="animate-fade-in pointer-events-none relative grid select-none gap-2"
role="presentation"
>
<div
className={clsx(
"rounded-lg bg-slate-800 p-4 shadow transition",
state.messageVisible ? "opacity-100" : "-translate-y-2 opacity-0",
)}
>
<div className="flex items-start gap-4">
<div className="h-12 w-12 rounded-full bg-black/25 bg-contain bg-no-repeat p-2">
<img
src={blobComfyUrl}
alt=""
className="h-full w-full scale-90 object-contain"
/>
</div>
<div>
<p className="font-bold">comfybot</p>
<p>this button was clicked {state.count} times</p>
<div className="mt-2 flex flex-row gap-3">
<div
ref={addRef}
className="rounded bg-emerald-700 px-3 py-1.5 text-sm text-white"
>
+1
</div>
<div
ref={deleteRef}
className="rounded bg-red-700 px-3 py-1.5 text-sm text-white"
>
🗑 delete
</div>
</div>
</div>
</div>
</div>
<div
className="rounded-lg bg-slate-700 px-4 pb-2 pt-1.5 shadow"
ref={chatInputRef}
>
<span
className={clsx(
"text-sm after:relative after:-left-[2px] after:-top-px after:content-[attr(data-after)]",
state.chatInputCursorVisible
? "after:opacity-100"
: "after:opacity-0",
)}
data-after="|"
>
{state.chatInputText || (
<span className="absolute block translate-y-1 opacity-50">
Message #showing-off-reacord
</span>
)}
</span>
</div>
<img
src={cursorUrl}
alt=""
className="absolute scale-75 bg-transparent transition-all duration-500"
style={{ left: state.cursorLeft, bottom: state.cursorBottom }}
ref={cursorRef}
/>
</div>
)
}

View File

@@ -1,43 +0,0 @@
---
import "@fontsource/jetbrains-mono/500.css"
import "@fontsource/rubik/variable.css"
import packageJson from "reacord/package.json"
import bannerUrl from "~/assets/banner.png"
import faviconUrl from "~/assets/favicon.png"
import "~/styles/tailwind.css"
---
<!DOCTYPE html>
<html lang="en" class="bg-slate-900 text-slate-100">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={packageJson.description} />
<meta name="theme-color" content="#21754b" />
<meta property="og:url" content="https://reacord.mapleleaf.dev/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Reacord" />
<meta
property="og:description"
content="Create interactive Discord messages using React"
/>
<meta property="og:image" content={bannerUrl} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:domain" content="reacord.mapleleaf.dev" />
<meta name="twitter:url" content="https://reacord.mapleleaf.dev/" />
<meta name="twitter:title" content="Reacord" />
<meta
name="twitter:description"
content="Create interactive Discord messages using React"
/>
<meta name="twitter:image" content={bannerUrl} />
<title>Reacord</title>
<link rel="icon" href={faviconUrl} />
</head>
<body>
<slot />
</body>
</html>

View File

@@ -1,81 +0,0 @@
---
import {
ArrowTopRightOnSquareIcon,
CodeBracketIcon,
DocumentTextIcon,
} from "@heroicons/react/20/solid"
import { Bars3Icon } from "@heroicons/react/24/outline"
import { getCollection } from "astro:content"
import AppLogo from "./app-logo.astro"
import ExternalLink from "./external-link.astro"
import MenuItem from "./menu-item.astro"
import Menu from "./menu.astro"
const links = [
{
href: "/guides/getting-started",
label: "Guides",
icon: DocumentTextIcon,
component: "a",
prefetch: true,
},
{
href: "/api/",
label: "API Reference",
icon: CodeBracketIcon,
component: "a",
},
{
href: "https://github.com/itsMapleLeaf/reacord",
label: "GitHub",
icon: ArrowTopRightOnSquareIcon,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
component: ExternalLink,
},
]
const guides = await getCollection("guides")
---
<nav class="flex h-16 items-center justify-between">
<a href="/">
<AppLogo class="w-32" />
<span class="sr-only">Home</span>
</a>
<div class="hidden gap-4 md:flex">
{
links.map((link) => (
<link.component
href={link.href}
class="link inline-flex items-center gap-1"
rel={link.prefetch ? "prefetch" : undefined}
>
<link.icon className="inline-icon" />
{link.label}
</link.component>
))
}
</div>
<Menu>
<Fragment slot="button">
<Bars3Icon className="w-6" />
<span class="sr-only">Menu</span>
</Fragment>
{
links.map((link) => (
<link.component href={link.href}>
<MenuItem icon={link.icon} label={link.label} />
</link.component>
))
}
<hr class="border-black/25" />
{
guides.map((guide) => (
<a href={`/guides/${guide.slug}`} rel="prefetch">
<MenuItem icon={DocumentTextIcon} label={guide.data.title} />
</a>
))
}
</Menu>
</nav>

View File

@@ -1,13 +0,0 @@
---
export interface Props {
icon: (props: { class?: string; className?: string }) => unknown
label: string
}
---
<div
class="flex w-full items-center gap-1 px-3 py-2 text-left font-medium opacity-50 transition hover:text-emerald-500 hover:opacity-100"
>
<Astro.props.icon class="inline-icon" className="inline-icon" />
<span class="flex-1">{Astro.props.label}</span>
</div>

View File

@@ -1,30 +0,0 @@
<details class="md:hidden relative" data-menu>
<summary
class="list-none p-2 -m-2 cursor-pointer hover:text-emerald-500 transition"
>
<slot name="button" />
</summary>
<div
class="w-48 max-h-[calc(100vh-5rem)] bg-slate-800 shadow rounded-lg overflow-x-hidden overflow-y-auto top-[calc(100%+8px)] right-0 absolute z-10"
>
<slot />
</div>
</details>
<script>
for (const menu of document.querySelectorAll<HTMLDetailsElement>(
"[data-menu]",
)) {
window.addEventListener("click", (event) => {
if (!menu.contains(event.target as Node)) {
menu.open = false
}
})
menu.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
menu.open = false
menu.querySelector("summary")!.focus()
}
})
}
</script>

View File

@@ -1,17 +0,0 @@
---
export type Props = astroHTML.JSX.AnchorHTMLAttributes & {
href: string
}
const removeTrailingSlash = (str: string) => str.replace(/\/$/, "")
const linkUrl = new URL(Astro.props.href, Astro.url)
const isActive =
removeTrailingSlash(Astro.url.pathname) ===
removeTrailingSlash(linkUrl.pathname)
---
<a {...Astro.props} data-active={isActive || undefined}>
<slot />
</a>

View File

@@ -1,10 +0,0 @@
import { defineCollection, z } from "astro:content"
export const collections = {
guides: defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
}),
}),
}

View File

@@ -1,52 +0,0 @@
---
title: Getting Started
description: Learn how to get started with Reacord.
slug: getting-started
---
# Getting Started
These guides assume some familiarity with JavaScript, [React](https://reactjs.org), [Discord.js](https://discord.js.org) and the [Discord API](https://discord.dev). Keep these pages as reference if you need it.
## Setup from template
[Use this starter template](https://github.com/itsMapleLeaf/reacord-starter) to get off the ground quickly.
## Adding to an existing project
Install Reacord and dependencies:
```bash
# npm
npm install reacord react discord.js
# yarn
yarn add reacord react discord.js
# pnpm
pnpm add reacord react discord.js
```
Create a Discord.js client and a Reacord instance:
```js
// main.jsx
import { Client } from "discord.js"
import { ReacordDiscordJs } from "reacord"
const client = new Client()
const reacord = new ReacordDiscordJs(client)
client.on("ready", () => {
console.log("Ready!")
})
await client.login(process.env.BOT_TOKEN)
```
To use JSX in your code, run it with [tsx](https://npm.im/tsx):
```bash
npm install tsx
tsx main.tsx
```

View File

@@ -1,166 +0,0 @@
---
title: Sending Messages
description: Sending messages by creating Reacord instances
slug: sending-messages
---
# Sending Messages with Instances
You can send messages via Reacord to a channel like so.
```jsx
const channelId = "abc123deadbeef"
client.on("ready", () => {
reacord.send(channelId, "Hello, world!")
})
```
The `.send()` function creates a **Reacord instance**. You can pass strings, numbers, or anything that can be rendered by React, such as JSX!
Components rendered through this instance can include state and effects, and the message on Discord will update automatically.
```jsx
function Uptime() {
const [startTime] = useState(Date.now())
const [currentTime, setCurrentTime] = useState(Date.now())
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(Date.now())
}, 3000)
return () => clearInterval(interval)
}, [])
return <>this message has been shown for {currentTime - startTime}ms</>
}
client.on("ready", () => {
reacord.send(channelId, <Uptime />)
})
```
The instance can be rendered to multiple times, which will update the message each time.
```jsx
const Hello = ({ subject }) => <>Hello, {subject}!</>
client.on("ready", () => {
const instance = reacord.send(channel)
instance.render(<Hello subject="World" />)
instance.render(<Hello subject="Moon" />)
})
```
## Cleaning Up Instances
If you no longer want to use the instance, you can clean it up in a few ways:
- `instance.destroy()` - This will remove the message.
- `instance.deactivate()` - This will keep the message, but it will disable the components on the message, and no longer listen to user interactions.
By default, Reacord has a max limit on the number of active instances, and deactivates older instances to conserve memory. This can be configured through the Reacord options:
```js
const reacord = new ReacordDiscordJs(client, {
// after sending four messages,
// the first one will be deactivated
maxInstances: 3,
})
```
## Discord Slash Commands
<aside>
This section also applies to other kinds of application commands, such as context menu commands.
</aside>
To reply to a command interaction, use the `.reply()` function. This function returns an instance that works the same way as the one from `.send()`. Here's an example:
```jsx
import { Client } from "discord.js"
import { Button, ReacordDiscordJs } from "reacord"
import * as React from "react"
const client = new Client({ intents: [] })
const reacord = new ReacordDiscordJs(client)
client.on("ready", () => {
client.application?.commands.create({
name: "ping",
description: "pong!",
})
})
client.on("interactionCreate", (interaction) => {
if (interaction.isCommand() && interaction.commandName === "ping") {
// Use the reply() function instead of send
reacord.reply(interaction, <>pong!</>)
}
})
client.login(process.env.DISCORD_TOKEN)
```
<aside>
This example uses <a href="https://discord.com/developers/docs/interactions/application-commands#registering-a-command">global commands</a>, so the command might take a while to show up 😅
</aside>
However, the process of creating commands can get really repetitive and error-prone. A command framework could help with this, or you could make a small helper:
```jsx
function handleCommands(client, commands) {
client.on("ready", () => {
for (const { name, description } of commands) {
client.application?.commands.create({ name, description })
}
})
client.on("interactionCreate", (interaction) => {
if (interaction.isCommand()) {
for (const command of commands) {
if (interaction.commandName === command.name) {
command.run(interaction)
}
}
}
})
}
```
```jsx
handleCommands(client, [
{
name: "ping",
description: "pong!",
run: (interaction) => {
reacord.reply(interaction, <>pong!</>)
},
},
{
name: "hi",
description: "say hi",
run: (interaction) => {
reacord.reply(interaction, <>hi</>)
},
},
])
```
## Ephemeral Command Replies
Ephemeral replies are replies that only appear for one user. To create them, use the `.ephemeralReply()` function.
```tsx
handleCommands(client, [
{
name: "pong",
description: "pong, but in secret",
run: (interaction) => {
reacord.ephemeralReply(interaction, <>(pong)</>)
},
},
])
```
The `ephemeralReply` function also returns an instance, but ephemeral replies cannot be updated via `instance.render()`. You can `.deactivate()` them, but `.destroy()` will not delete the message; only the user can hide it from view.

View File

@@ -1,63 +0,0 @@
---
title: Embeds
description: Using embed components
slug: embeds
---
# Embeds
Reacord comes with an `<Embed />` component for sending rich embeds.
```jsx
import { Embed } from "reacord"
function FancyMessage({ title, description }) {
return (
<Embed
title={title}
description={description}
color={0x00ff00}
timestamp={Date.now()}
/>
)
}
```
```jsx
reacord.send(channelId, <FancyMessage title="Hello" description="World" />)
```
Reacord also comes with multiple embed components, for defining embeds on a piece-by-piece basis. This enables composition:
```jsx
import { Embed, EmbedTitle } from "reacord"
function FancyDetails({ title, description }) {
return (
<>
<EmbedTitle>{title}</EmbedTitle>
{/* embed descriptions are just text */}
{description}
</>
)
}
function FancyMessage({ children }) {
return (
<Embed color={0x00ff00} timestamp={Date.now()}>
{children}
</Embed>
)
}
```
```jsx
reacord.send(
channelId,
<FancyMessage>
<FancyDetails title="Hello" description="World" />
</FancyMessage>,
)
```
See the [API Reference](/api/index.html#EmbedAuthorProps) for the full list of embed components.

View File

@@ -1,46 +0,0 @@
---
title: Buttons
description: Using button components
slug: buttons
---
# Buttons
Use the `<Button />` component to create a message with a button, and use the `onClick` callback to respond to button clicks.
```jsx
import { Button } from "reacord"
function Counter() {
const [count, setCount] = useState(0)
return (
<>
You've clicked the button {count} times.
<Button label="+1" onClick={() => setCount(count + 1)} />
</>
)
}
```
The `onClick` callback receives an `event` object. It includes some information, such as the user who clicked the button, and functions for creating new replies in response. These functions return message instances.
```jsx
import { Button } from "reacord"
function TheButton() {
function handleClick(event) {
const name = event.guild.member.displayName || event.user.username
const publicReply = event.reply(`${name} clicked the button. wow`)
setTimeout(() => publicReply.destroy(), 3000)
const privateReply = event.ephemeralReply("good job, you clicked it")
privateReply.deactivate() // we don't need to listen to updates on this
}
return <Button label="click me i dare you" onClick={handleClick} />
}
```
See the [API reference](/api/index.html#ButtonProps) for more information.

View File

@@ -1,24 +0,0 @@
---
title: Links
description: Using link components
slug: links
---
# Links
In Discord, links are a type of button, and they work similarly. Clicking on it leads you to the given URL. They only have one style, and can't be listened to for clicks.
```jsx
import { Link } from "reacord"
function AwesomeLinks() {
return (
<>
<Link label="look at this" url="https://google.com" />
<Link label="wow" url="https://youtube.com/watch?v=dQw4w9WgXcQ" />
</>
)
}
```
See the [API reference](/api/index.html#Link) for more information.

View File

@@ -1,70 +0,0 @@
---
title: Select Menus
description: Using select menu components
slug: select-menus
---
# Select Menus
To create a select menu, use the `Select` component, and pass a list of `Option` components as children. Use the `value` prop to set a currently selected value. You can respond to changes in the value via `onChangeValue`.
```jsx
export function FruitSelect({ onConfirm }) {
const [value, setValue] = useState()
return (
<>
<Select
placeholder="choose a fruit"
value={value}
onChangeValue={setValue}
>
<Option value="🍎" />
<Option value="🍌" />
<Option value="🍒" />
</Select>
<Button
label="confirm"
disabled={value == undefined}
onClick={() => {
if (value) onConfirm(value)
}}
/>
</>
)
}
```
```jsx
const instance = reacord.send(
channelId,
<FruitSelect
onConfirm={(value) => {
instance.render(`you chose ${value}`)
instance.deactivate()
}}
/>,
)
```
For a multi-select, use the `multiple` prop, then you can use `values` and `onChangeMultiple` to handle multiple values.
```tsx
export function FruitSelect({ onConfirm }) {
const [values, setValues] = useState([])
return (
<Select
placeholder="choose a fruit"
values={values}
onChangeMultiple={setValues}
>
<Option value="🍎" />
<Option value="🍌" />
<Option value="🍒" />
</Select>
)
}
```
The Select component accepts a variety of props beyond what's shown here. See the [API reference](/api/index.html#SelectChangeEvent) for more information.

View File

@@ -1,26 +0,0 @@
---
title: useInstance
description: Using useInstance to get the current instance within a component
slug: use-instance
---
# useInstance
You can use `useInstance` to get the current [instance](/guides/sending-messages) within a component. This can be used to let a component destroy or deactivate itself.
```jsx
import { Button, useInstance } from "reacord"
function SelfDestruct() {
const instance = useInstance()
return (
<Button
style="danger"
label="delete this"
onClick={() => instance.destroy()}
/>
)
}
reacord.send(channelId, <SelfDestruct />)
```

View File

@@ -1,11 +0,0 @@
---
title: Using Reacord with other libraries
description: Adapting Reacord to another Discord library
slug: custom-adapters
---
# Using Reacord with other libraries
Reacord's core is built to be library agnostic, and can be adapted to libraries other than Discord.js. However, Discord.js is the only built-in adapter at the moment, and the adapter API is still a work in progress.
If you're interested in creating a custom adapter, [see the code for the Discord.js adapter as an example](https://github.com/itsMapleLeaf/reacord/blob/main/packages/reacord/library/core/reacord-discord-js.ts). Feel free to [create an issue on GitHub](https://github.com/itsMapleLeaf/reacord/issues/new) if you run into issues.

View File

@@ -1,3 +0,0 @@
/// <reference types="astro/client" />
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../.astro/types.d.ts" />

View File

@@ -1,95 +0,0 @@
---
import type { GetStaticPaths } from "astro"
import { getCollection, type CollectionEntry } from "astro:content"
import AppFooter from "~/components/app-footer.astro"
import Layout from "~/components/layout.astro"
import MainNavigation from "~/components/main-navigation.astro"
import NavLink from "~/components/nav-link.astro"
export interface Props {
guide: CollectionEntry<"guides">
}
export const getStaticPaths: GetStaticPaths = async () => {
const guides = await getCollection("guides")
return guides.map((guide) => ({
params: { slug: guide.slug },
props: { guide },
}))
}
const guides = await getCollection("guides")
const { Content } = await Astro.props.guide.render()
---
<Layout>
<div class="isolate">
<header
class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
>
<div class="container">
<MainNavigation />
</div>
</header>
<main class="container mt-8 flex items-start gap-4">
<nav
class="sticky top-24 hidden h-[calc(100vh-theme(spacing.28))] w-48 flex-col gap-3 md:flex"
>
<h2 class="text-2xl">Guides</h2>
<ul class="flex flex-col items-start gap-2">
{
guides.map((guide) => (
<li>
<NavLink
class="link data-[active]:link-active"
href={`/guides/${guide.slug}`}
rel="prefetch"
>
{guide.data.title}
</NavLink>
</li>
))
}
</ul>
<AppFooter class="mt-auto" />
</nav>
<article class="-mt-8 min-w-0 max-w-none flex-1 pb-8">
<Content />
</article>
</main>
<AppFooter class="mx-auto mb-4 text-center md:hidden" />
</div>
</Layout>
<style>
article :global(:where(h1, h2, h3, h4, h5, h6)) {
@apply mb-3 mt-8 font-light;
}
article :global(h1) {
@apply text-3xl lg:text-4xl;
}
article :global(h2) {
@apply text-2xl;
}
article :global(h3) {
@apply text-xl;
}
article :global(p) {
@apply my-3;
}
article :global(a) {
@apply font-medium text-emerald-400 hover:no-underline;
}
article :global(strong) {
@apply font-medium text-emerald-400;
}
article :global(code) {
@apply rounded border border-slate-800 bg-slate-950 px-1 py-0.5 leading-none text-slate-300;
}
article :global(pre) {
@apply my-4 overflow-x-auto rounded-md border border-slate-800 !bg-slate-950 px-4 py-3 font-monospace;
}
article :global(pre code) {
@apply border-none bg-transparent p-0;
}
</style>

View File

@@ -1,54 +0,0 @@
---
import dotsBackgroundUrl from "~/assets/dots-background.svg"
import AppFooter from "~/components/app-footer.astro"
import AppLogo from "~/components/app-logo.astro"
import { LandingAnimation } from "~/components/landing-animation"
import Layout from "~/components/layout.astro"
import MainNavigation from "~/components/main-navigation.astro"
---
<Layout>
<div
class="fixed inset-0 rotate-6 scale-125 opacity-20"
style={{ backgroundImage: `url(${dotsBackgroundUrl})` }}
>
</div>
<div class="relative flex min-h-screen min-w-0 flex-col gap-4 pb-4">
<header class="container">
<MainNavigation />
</header>
<div class="my-auto flex flex-col gap-4 px-4">
<AppLogo class="mx-auto w-full max-w-lg" />
<div class="isolate mx-auto h-44 w-full max-w-md">
<LandingAnimation client:only />
</div>
<p class="-mb-1 text-center text-lg font-light">
Create interactive Discord messages with React.
</p>
<div class="flex gap-4 self-center">
<a href="/guides/getting-started" class="button button-solid">
Get Started
</a>
<!-- <UncontrolledModal
button={(button) => (
<button {...button} class={buttonClass({ variant: "semiblack" })}>
Show Code
</button>
)}
>
<div class="text-sm sm:text-base">
<LandingCode />
</div>
</UncontrolledModal> -->
</div>
</div>
<div class="container text-center">
<AppFooter />
</div>
</div>
</Layout>

View File

@@ -1,59 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:focus {
@apply outline-none;
}
:focus-visible {
@apply ring-2 ring-inset ring-emerald-500;
}
pre,
code {
font-variant-ligatures: none;
}
}
@layer components {
.container {
@apply mx-auto w-full max-w-screen-lg px-4;
}
.inline-icon {
@apply inline w-5 align-sub;
}
.link {
@apply relative inline-block font-medium opacity-60 transition-opacity hover:opacity-100;
}
.link::after {
@apply absolute bottom-[-2px] block h-px w-full translate-y-[3px] bg-current opacity-0 transition content-[''];
}
.link:hover::after {
@apply -translate-y-px opacity-50;
}
.link-active {
@apply text-emerald-500 opacity-100;
}
.button {
@apply mt-4 inline-block rounded-lg bg-black/25 px-4 py-2.5 text-xl transition hover:-translate-y-0.5 hover:bg-black/40 hover:shadow active:translate-y-0 active:transition-none;
}
.button-solid {
@apply bg-emerald-700 hover:bg-emerald-800;
}
}
@layer utilities {
@keyframes fade-in {
from {
opacity: 0;
}
}
.animate-fade-in {
animation: fade-in 0.5s;
}
}

View File

@@ -1,7 +0,0 @@
import type { Config } from "tailwindcss"
import config from "../../tailwind.config.ts"
export default {
...config,
content: ["./src/**/*.{ts,tsx,md,astro}"],
} satisfies Config

View File

@@ -1,12 +0,0 @@
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist", "public/api"]
}

View File

@@ -1,22 +0,0 @@
{
"entryPoints": ["../reacord/library/main.ts"],
"out": ["public/api"],
"tsconfig": "../reacord/tsconfig.json",
"excludeInternal": true,
"excludePrivate": true,
"excludeProtected": true,
"categorizeByGroup": false,
"preserveWatchOutput": true,
"githubPages": false,
"readme": "none",
"categoryOrder": [
"Core",
"Embed",
"Button",
"Link",
"Select",
"Action Row",
"Component Event",
"*"
]
}

9699
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,2 @@
packages:
- packages/*
ignoredBuiltDependencies:
- esbuild

View File

@@ -1,16 +0,0 @@
export default {
theme: {
fontFamily: {
sans: ["RubikVariable", "sans-serif"],
monospace: ["'JetBrains Mono'", "monospace"],
},
boxShadow: {
DEFAULT: "0 2px 9px 0 rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3)",
},
extend: {},
},
corePlugins: {
container: false,
},
plugins: [],
}

View File

@@ -1,3 +1,3 @@
{
"extends": "@itsmapleleaf/configs/tsconfig"
"extends": "@itsmapleleaf/configs/tsconfig.bundler.json"
}

View File

@@ -1,5 +0,0 @@
{
"github": {
"silent": true
}
}