31 Commits

Author SHA1 Message Date
Darius
b210670b2a Merge pull request #31 from itsMapleLeaf/changeset-release/main 2023-09-27 23:21:57 -05:00
github-actions[bot]
2b9110bf2c Version Packages 2023-09-28 04:20:36 +00:00
Darius
5d4dde4e0c Merge pull request #30 from itsMapleLeaf/renderer-bug-fixes 2023-09-27 23:20:04 -05:00
itsMapleLeaf
31baa23076 format astro files 2023-09-27 23:18:36 -05:00
itsMapleLeaf
d76f316bb7 ensure action rows handle child interactions 2023-09-27 23:15:24 -05:00
itsMapleLeaf
47b0645a90 fix linter warnings 2023-09-27 23:09:21 -05:00
itsMapleLeaf
0bab505994 fix deprecated method 2023-09-27 23:09:21 -05:00
itsMapleLeaf
104b175931 edit if deferred or replied 2023-09-27 23:08:41 -05:00
itsMapleLeaf
156cf90919 set component interaction in loop
...only when an interaction was handled
2023-09-27 23:08:35 -05:00
Darius
b463ce3cf4 Merge pull request #27 from itsMapleLeaf/update-lint 2023-09-27 13:32:49 -05:00
itsMapleLeaf
576dd2e35e manual fixes, disable some errors 2023-09-27 13:29:20 -05:00
itsMapleLeaf
0d4294ee8c upgrade deps + remove unneeded 2023-09-27 12:33:40 -05:00
itsMapleLeaf
25fcc53d91 only list formatted files 2023-09-27 12:26:07 -05:00
itsMapleLeaf
34bc293df5 Merge branch 'update-lint' of https://github.com/itsMapleLeaf/reacord into update-lint 2023-09-27 12:24:00 -05:00
itsMapleLeaf
b7b237f2f5 less wordy name 2023-09-27 12:23:56 -05:00
itsMapleLeaf
e2c3de4fae Apply automatic changes 2023-09-27 17:23:41 +00:00
itsMapleLeaf
ffe0a86a33 merge and rename workflows 2023-09-27 12:23:24 -05:00
itsMapleLeaf
6ce9241080 run other scripts + fix always run 2023-09-27 12:22:09 -05:00
itsMapleLeaf
5d96d517df add cache and autocommit 2023-09-27 12:10:05 -05:00
itsMapleLeaf
2c706f6791 update lint configs and scripts 2023-09-27 12:07:32 -05:00
itsMapleLeaf
2abb61493e Merge branch 'rewrite' 2023-08-16 20:44:42 -05:00
itsMapleLeaf
3db1013b74 also sync before lint 2023-08-16 20:44:35 -05:00
itsMapleLeaf
3e2c6ba5d6 rename test job 2023-08-16 20:43:10 -05:00
itsMapleLeaf
0172534d13 actually remove tailwind typography 2023-08-16 20:41:59 -05:00
itsMapleLeaf
1a49423beb run astro sync before typecheck 2023-08-16 20:41:06 -05:00
itsMapleLeaf
3824859352 add github profile link 2023-08-16 20:34:35 -05:00
itsMapleLeaf
0dad3c9ecd style fixes and improvements 2023-08-16 20:33:53 -05:00
itsMapleLeaf
eea1a7ee9d solve some weird inclusion errors 2023-08-16 20:02:58 -05:00
itsMapleLeaf
e9e5a1617b tooling overhaul 2023-08-16 19:32:28 -05:00
itsMapleLeaf
7ac1a9cdce improve typecheck setup 2023-03-12 19:44:59 -05:00
itsMapleLeaf
33841a0c84 remove some extra scripts 2023-03-12 19:31:33 -05:00
115 changed files with 6598 additions and 6602 deletions

View File

@@ -1,38 +0,0 @@
require("@rushstack/eslint-patch/modern-module-resolution")
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [require.resolve("@itsmapleleaf/configs/eslint")],
ignorePatterns: [
"**/node_modules/**",
"**/.cache/**",
"**/build/**",
"**/dist/**",
"**/coverage/**",
"**/public/**",
],
parserOptions: {
project: require.resolve("./tsconfig.base.json"),
extraFileExtensions: [".astro"],
},
overrides: [
{
files: ["packages/website/cypress/**"],
parserOptions: {
project: require.resolve("./packages/website/cypress/tsconfig.json"),
},
},
{
files: ["*.astro"],
parser: "astro-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
},
rules: {
"react/no-unknown-property": "off",
"react/jsx-key": "off",
"react/jsx-no-undef": "off",
},
},
],
}

View File

@@ -1,53 +0,0 @@
name: main
on:
push:
branches: [main]
pull_request:
env:
TEST_BOT_TOKEN: ${{ secrets.TEST_BOT_TOKEN }}
TEST_CHANNEL_ID: ${{ secrets.TEST_CHANNEL_ID }}
TEST_GUILD_ID: ${{ secrets.TEST_GUILD_ID }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
run-commands:
strategy:
fail-fast: false
matrix:
command:
# if these run in the same process, it dies,
# so we test them separate
- name: test reacord
run: pnpm -C packages/reacord test
# - name: test website
# run: pnpm -C packages/website test
- name: build
run: pnpm --recursive run build
- name: lint
run: pnpm run lint
- name: typecheck
run: pnpm --recursive run typecheck
name: ${{ matrix.command.name }}
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: 7.13.4
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: ${{ matrix.command.run }}

View File

@@ -13,22 +13,15 @@ jobs:
name: release name: release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - uses: actions/checkout@v3
uses: actions/checkout@v3 - uses: pnpm/action-setup@v2
- name: setup pnpm
uses: pnpm/action-setup@v2
with: with:
version: 7.13.4 version: 8
- uses: actions/setup-node@v3
- name: setup node
uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 18
cache: pnpm cache: pnpm
- run: pnpm install --frozen-lockfile
- name: install deps
run: pnpm install --frozen-lockfile
- name: changesets release - name: changesets release
id: changesets id: changesets

44
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: tests
on:
push:
branches: [main]
pull_request:
env:
TEST_BOT_TOKEN: ${{ secrets.TEST_BOT_TOKEN }}
TEST_CHANNEL_ID: ${{ secrets.TEST_CHANNEL_ID }}
TEST_GUILD_ID: ${{ secrets.TEST_GUILD_ID }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: ${{ matrix.script }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
script:
- lint
- build
- test
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run ${{ matrix.script }}
- uses: stefanzweifel/git-auto-commit-action@v4
if: always()

3
.gitignore vendored
View File

@@ -5,8 +5,7 @@ coverage
.env .env
*.code-workspace *.code-workspace
.pnpm-debug.log .pnpm-debug.log
build build
.cache .cache
.vercel .vercel
*.tsbuildinfo

View File

@@ -1,7 +1,3 @@
node_modules
dist
coverage
pnpm-lock.yaml pnpm-lock.yaml
build /packages/website/public/api
.cache .astro
packages/website/public/api

View File

@@ -1,14 +0,0 @@
const base = require("@itsmapleleaf/configs/prettier")
module.exports = {
...base,
plugins: [require.resolve("prettier-plugin-astro")],
overrides: [
{
files: "*.astro",
options: {
parser: "astro",
},
},
],
}

View File

@@ -2,25 +2,46 @@
"name": "reacord-monorepo", "name": "reacord-monorepo",
"private": true, "private": true,
"scripts": { "scripts": {
"lint": "eslint --ext js,ts,tsx .", "lint": "run-s --continue-on-error lint:*",
"lint-fix": "pnpm lint -- --fix", "lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
"format": "prettier --write .", "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\"",
"test": "vitest",
"build": "pnpm -r run build", "build": "pnpm -r run build",
"build:website": "pnpm --filter website... run build",
"start": "pnpm -C packages/website run start", "start": "pnpm -C packages/website run start",
"start:website": "pnpm -C packages/website run start",
"release": "pnpm -r run build && changeset publish" "release": "pnpm -r run build && changeset publish"
}, },
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.25.0", "@changesets/cli": "^2.26.2",
"@itsmapleleaf/configs": "^1.1.7", "@itsmapleleaf/configs": "github:itsMapleLeaf/configs",
"@rushstack/eslint-patch": "^1.2.0", "eslint": "^8.50.0",
"@types/eslint": "^8.4.6", "npm-run-all": "^4.1.5",
"astro-eslint-parser": "^0.12.0", "prettier": "^3.0.3",
"eslint": "^8.36.0", "react": "^18.2.0",
"prettier": "^2.7.1", "tailwindcss": "^3.3.3",
"prettier-plugin-astro": "^0.8.0", "typescript": "^5.2.2",
"typescript": "^4.8.4" "vitest": "^0.34.5"
}, },
"resolutions": { "prettier": "@itsmapleleaf/configs/prettier",
"esbuild": "latest" "eslintConfig": {
"extends": [
"./node_modules/@itsmapleleaf/configs/eslint.config.cjs",
"./node_modules/@itsmapleleaf/configs/eslint.config.react.cjs"
],
"ignorePatterns": [
"node_modules",
"dist",
"packages/website/public/api"
],
"rules": {
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/require-await": "off"
}
} }
} }

View File

@@ -1,9 +1,9 @@
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
import type { import type {
CamelCasedPropertiesDeep, CamelCasedPropertiesDeep,
SnakeCasedPropertiesDeep, SnakeCasedPropertiesDeep,
} from "type-fest" } from "type-fest"
import { expect, test } from "vitest" import { expect, test } from "vitest"
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
test("camelCaseDeep", () => { test("camelCaseDeep", () => {
const input = { const input = {

View File

@@ -2,6 +2,7 @@ import { camelCase, isObject, snakeCase } from "lodash-es"
import type { import type {
CamelCasedPropertiesDeep, CamelCasedPropertiesDeep,
SnakeCasedPropertiesDeep, SnakeCasedPropertiesDeep,
UnknownRecord,
} from "type-fest" } from "type-fest"
function convertKeyCaseDeep<Input, Output>( function convertKeyCaseDeep<Input, Output>(
@@ -18,11 +19,11 @@ function convertKeyCaseDeep<Input, Output>(
) as unknown as Output ) as unknown as Output
} }
const output: any = {} const output = {} as UnknownRecord
for (const [key, value] of Object.entries(input)) { for (const [key, value] of Object.entries(input)) {
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey) output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
} }
return output return output as Output
} }
export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> { export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> {

View File

@@ -1,7 +1,7 @@
/** /** For narrowing instance types with array.filter */
* for narrowing instance types with array.filter
*/
export const isInstanceOf = export const isInstanceOf =
<T>(Constructor: new (...args: any[]) => T) => <Instance, Args extends unknown[]>(
(value: unknown): value is T => constructor: new (...args: Args) => Instance,
value instanceof Constructor ) =>
(value: unknown): value is Instance =>
value instanceof constructor

View File

@@ -1,7 +1,3 @@
export function isObject<T>( export function isObject(value: unknown): value is object {
value: T,
): value is Exclude<T, Primitive | AnyFunction> {
return typeof value === "object" && value !== null return typeof value === "object" && value !== null
} }
type Primitive = string | number | boolean | undefined | null
type AnyFunction = (...args: any[]) => any

7
packages/helpers/json.ts Normal file
View File

@@ -0,0 +1,7 @@
export function safeJsonStringify(value: unknown): string {
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}

View File

@@ -0,0 +1,7 @@
import { expect, test } from "vitest"
import { omit } from "./omit.ts"
test("omit", () => {
const subject = { a: 1, b: true }
expect(omit(subject, ["a"])).toStrictEqual({ b: true })
})

View File

@@ -0,0 +1,3 @@
import { omit } from "./omit.ts"
omit({ a: 1, b: true }, ["a"]) satisfies { b: boolean }

View File

@@ -1,13 +1,10 @@
export function omit<Subject extends object, Key extends PropertyKey>( export function omit<Subject extends object, Key extends PropertyKey>(
subject: Subject, subject: Subject,
keys: Key[], keys: Key[],
// hack: using a conditional type preserves union types ) {
): Subject extends any ? Omit<Subject, Key> : never { const keySet = new Set<PropertyKey>(keys)
const result: any = {} return Object.fromEntries(
for (const key in subject) { Object.entries(subject).filter(([key]) => !keySet.has(key)),
if (!keys.includes(key as unknown as Key)) { // hack: conditional type preserves unions
result[key] = subject[key] ) as Subject extends unknown ? Omit<Subject, Key> : never
}
}
return result
} }

View File

@@ -2,10 +2,13 @@
"name": "@reacord/helpers", "name": "@reacord/helpers",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": {
"typecheck": "tsc -b"
},
"dependencies": { "dependencies": {
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.9",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"type-fest": "^2.17.0", "type-fest": "^4.3.2",
"vitest": "^0.18.1" "vitest": "^0.34.5"
} }
} }

View File

@@ -1,15 +1,11 @@
import type { LoosePick, UnknownRecord } from "./types" import type { LoosePick } from "./types"
export function pick<T, K extends keyof T | PropertyKey>( export function pick<T extends object, K extends keyof T | PropertyKey>(
object: T, object: T,
keys: K[], keys: K[],
): LoosePick<T, K> { ) {
const result: any = {} const keySet = new Set<PropertyKey>(keys)
for (const key of keys) { return Object.fromEntries(
const value = (object as UnknownRecord)[key] Object.entries(object).filter(([key]) => keySet.has(key)),
if (value !== undefined) { ) as LoosePick<T, K>
result[key] = value
}
}
return result
} }

View File

@@ -3,7 +3,7 @@ import type { PruneNullishValues } from "./prune-nullish-values"
import { pruneNullishValues } from "./prune-nullish-values" import { pruneNullishValues } from "./prune-nullish-values"
test("pruneNullishValues", () => { test("pruneNullishValues", () => {
type InputType = { interface InputType {
a: string a: string
b: string | null | undefined b: string | null | undefined
c?: string c?: string
@@ -15,7 +15,6 @@ test("pruneNullishValues", () => {
const input: InputType = { const input: InputType = {
a: "a", a: "a",
// eslint-disable-next-line unicorn/no-null
b: null, b: null,
c: undefined, c: undefined,
d: { d: {

View File

@@ -1,21 +1,25 @@
import { isObject } from "./is-object" import { isObject } from "./is-object"
export function pruneNullishValues<T>(input: T): PruneNullishValues<T> { export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
if (Array.isArray(input)) {
return input.filter(Boolean).map((item) => pruneNullishValues(item)) as any
}
if (!isObject(input)) { if (!isObject(input)) {
return input as any return input as PruneNullishValues<T>
} }
const result: any = {} if (Array.isArray(input)) {
for (const [key, value] of Object.entries(input as any)) { return input
.filter(Boolean)
.map(
(item) => pruneNullishValues(item) as unknown,
) as PruneNullishValues<T>
}
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(input)) {
if (value != undefined) { if (value != undefined) {
result[key] = pruneNullishValues(value) result[key] = pruneNullishValues(value)
} }
} }
return result return result as PruneNullishValues<T>
} }
export type PruneNullishValues<Input> = Input extends object export type PruneNullishValues<Input> = Input extends object

View File

@@ -1,5 +1,5 @@
import { setTimeout } from "node:timers/promises"
import { toError } from "./to-error.js" import { toError } from "./to-error.js"
import { setTimeout } from "node:timers/promises"
export async function rejectAfter( export async function rejectAfter(
timeMs: number, timeMs: number,

View File

@@ -7,7 +7,7 @@ export async function retryWithTimeout<T>(
callback: () => Promise<T> | T, callback: () => Promise<T> | T,
): Promise<T> { ): Promise<T> {
const startTime = Date.now() const startTime = Date.now()
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition
while (true) { while (true) {
try { try {
return await callback() return await callback()

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.base.json"
}

View File

@@ -0,0 +1,4 @@
import { type LooseOmit, type LoosePick, typeEquals } from "./types.ts"
typeEquals<LoosePick<{ a: 1; b: 2 }, "a">, { a: 1 }>(true)
typeEquals<LooseOmit<{ a: 1; b: 2 }, "a">, { b: 2 }>(true)

View File

@@ -1,11 +1,21 @@
export type MaybePromise<T> = T | Promise<T> import { raise } from "./raise.ts"
export type MaybePromise<T> = T | PromiseLike<T>
export type ValueOf<Type> = Type extends ReadonlyArray<infer Value> export type ValueOf<Type> = Type extends ReadonlyArray<infer Value>
? Value ? Value
: Type[keyof Type] : Type[keyof Type]
export type UnknownRecord = Record<PropertyKey, unknown> export type LoosePick<Shape, Keys extends PropertyKey> = Simplify<{
[Key in Extract<Keys, keyof Shape>]: Shape[Key]
}>
export type LoosePick<Shape, Keys extends PropertyKey> = { export type LooseOmit<Shape, Keys extends PropertyKey> = Simplify<{
[Key in Keys]: Shape extends Record<Key, infer Value> ? Value : never [Key in Exclude<keyof Shape, Keys>]: Shape[Key]
} }>
export type Simplify<T> = { [Key in keyof T]: T[Key] } & NonNullable<unknown>
export const typeEquals = <A, B>(
_result: A extends B ? (B extends A ? true : false) : false,
) => raise("typeEquals() should not be called at runtime")

View File

@@ -1,9 +1,10 @@
import { setTimeout } from "node:timers/promises" import { setTimeout } from "node:timers/promises"
import type { MaybePromise } from "./types.ts"
const maxTime = 1000 const maxTime = 1000
export async function waitFor<Result>( export async function waitFor<Result>(
predicate: () => Result, predicate: () => MaybePromise<Result>,
): Promise<Awaited<Result>> { ): Promise<Awaited<Result>> {
const startTime = Date.now() const startTime = Date.now()
let lastError: unknown let lastError: unknown
@@ -17,5 +18,6 @@ export async function waitFor<Result>(
} }
} }
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw lastError ?? new Error("Timeout") throw lastError ?? new Error("Timeout")
} }

View File

@@ -7,7 +7,7 @@ export function withLoggedMethodCalls<T extends object>(value: T) {
if (typeof value !== "function") { if (typeof value !== "function") {
return value return value
} }
return (...values: any[]) => { return (...values: unknown[]) => {
console.info( console.info(
`${String(property)}(${values `${String(property)}(${values
.map((value) => .map((value) =>
@@ -17,7 +17,7 @@ export function withLoggedMethodCalls<T extends object>(value: T) {
) )
.join(", ")})`, .join(", ")})`,
) )
return value.apply(target, values) return value.apply(target, values) as unknown
} }
}, },
}) as T }) as T

View File

@@ -1,5 +1,14 @@
# reacord # reacord
## 0.5.3
### Patch Changes
- 104b175: ensure message is edited from arbitrary component updates
- 156cf90: fix interaction handling
- 0bab505: fix DJS deprecation warning on isStringSelectMenu
- d76f316: ensure action rows handle child interactions
## 0.5.2 ## 0.5.2
### Patch Changes ### Patch Changes

1
packages/reacord/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="@total-typescript/ts-reset" />

View File

@@ -1,52 +1,49 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import type { ReacordInstance } from "./instance" import type { ReacordInstance } from "./instance"
/** /** @category Component Event */
* @category Component Event export interface ComponentEvent {
*/
export type ComponentEvent = {
/** /**
* The message associated with this event. * The message associated with this event. For example: with a button click,
* For example: with a button click,
* this is the message that the button is on. * this is the message that the button is on.
*
* @see https://discord.com/developers/docs/resources/channel#message-object * @see https://discord.com/developers/docs/resources/channel#message-object
*/ */
message: MessageInfo message: MessageInfo
/** /**
* The channel that this event occurred in. * The channel that this event occurred in.
*
* @see https://discord.com/developers/docs/resources/channel#channel-object * @see https://discord.com/developers/docs/resources/channel#channel-object
*/ */
channel: ChannelInfo channel: ChannelInfo
/** /**
* The user that triggered this event. * The user that triggered this event.
*
* @see https://discord.com/developers/docs/resources/user#user-object * @see https://discord.com/developers/docs/resources/user#user-object
*/ */
user: UserInfo user: UserInfo
/** /**
* The guild that this event occurred in. * The guild that this event occurred in.
*
* @see https://discord.com/developers/docs/resources/guild#guild-object * @see https://discord.com/developers/docs/resources/guild#guild-object
*/ */
guild?: GuildInfo guild?: GuildInfo
/** /** Create a new reply to this event. */
* Create a new reply to this event.
*/
reply(content?: ReactNode): ReacordInstance reply(content?: ReactNode): ReacordInstance
/** /**
* Create an ephemeral reply to this event, * Create an ephemeral reply to this event, shown only to the user who
* shown only to the user who triggered it. * triggered it.
*/ */
ephemeralReply(content?: ReactNode): ReacordInstance ephemeralReply(content?: ReactNode): ReacordInstance
} }
/** /** @category Component Event */
* @category Component Event export interface ChannelInfo {
*/
export type ChannelInfo = {
id: string id: string
name?: string name?: string
topic?: string topic?: string
@@ -57,13 +54,11 @@ export type ChannelInfo = {
rateLimitPerUser?: number rateLimitPerUser?: number
} }
/** /** @category Component Event */
* @category Component Event export interface MessageInfo {
*/
export type MessageInfo = {
id: string id: string
channelId: string channelId: string
authorId: UserInfo authorId: string
member?: GuildMemberInfo member?: GuildMemberInfo
content: string content: string
timestamp: string timestamp: string
@@ -74,19 +69,15 @@ export type MessageInfo = {
mentions: string[] mentions: string[]
} }
/** /** @category Component Event */
* @category Component Event export interface GuildInfo {
*/
export type GuildInfo = {
id: string id: string
name: string name: string
member: GuildMemberInfo member: GuildMemberInfo
} }
/** /** @category Component Event */
* @category Component Event export interface GuildMemberInfo {
*/
export type GuildMemberInfo = {
id: string id: string
nick?: string nick?: string
displayName: string displayName: string
@@ -100,14 +91,12 @@ export type GuildMemberInfo = {
communicationDisabledUntil?: string communicationDisabledUntil?: string
} }
/** /** @category Component Event */
* @category Component Event export interface UserInfo {
*/
export type UserInfo = {
id: string id: string
username: string username: string
discriminator: string discriminator: string
tag: string tag: string
avatarUrl: string avatarUrl: string | null
accentColor?: number accentColor?: number
} }

View File

@@ -1,22 +1,23 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import type { MessageOptions } from "../../internal/message" import type { MessageOptions } from "../../internal/message"
import { Node } from "../../internal/node.js" import { Node } from "../../internal/node.js"
import type { ComponentInteraction } from "../../internal/interaction.js"
/** /**
* Props for an action row * Props for an action row
*
* @category Action Row * @category Action Row
*/ */
export type ActionRowProps = { export interface ActionRowProps {
children?: ReactNode children?: ReactNode
} }
/** /**
* An action row is a top-level container for message components. * An action row is a top-level container for message components.
* *
* You don't need to use this; Reacord automatically creates action rows for you. * You don't need to use this; Reacord automatically creates action rows for
* But this can be useful if you want a specific layout. * you. But this can be useful if you want a specific layout.
* *
* ```tsx * ```tsx
* // put buttons on two separate rows * // put buttons on two separate rows
@@ -37,11 +38,19 @@ export function ActionRow(props: ActionRowProps) {
) )
} }
class ActionRowNode extends Node<{}> { class ActionRowNode extends Node<ActionRowProps> {
override modifyMessageOptions(options: MessageOptions): void { override modifyMessageOptions(options: MessageOptions): void {
options.actionRows.push([]) options.actionRows.push([])
for (const child of this.children) { for (const child of this.children) {
child.modifyMessageOptions(options) child.modifyMessageOptions(options)
} }
} }
handleComponentInteraction(interaction: ComponentInteraction) {
for (const child of this.children) {
if (child.handleComponentInteraction(interaction)) {
return true
}
}
return false
}
} }

View File

@@ -2,9 +2,10 @@ import type { ReactNode } from "react"
/** /**
* Common props between button-like components * Common props between button-like components
*
* @category Button * @category Button
*/ */
export type ButtonSharedProps = { export interface ButtonSharedProps {
/** The text on the button. Rich formatting (markdown) is not supported here. */ /** The text on the button. Rich formatting (markdown) is not supported here. */
label?: ReactNode label?: ReactNode
@@ -12,13 +13,12 @@ export type ButtonSharedProps = {
disabled?: boolean disabled?: boolean
/** /**
* Renders an emoji to the left of the text. * Renders an emoji to the left of the text. Has to be a literal emoji
* Has to be a literal emoji character (e.g. 🍍), * character (e.g. 🍍), or an emoji code, like
* or an emoji code, like `<:plus_one:778531744860602388>`. * `<:plus_one:778531744860602388>`.
* *
* To get an emoji code, type your emoji in Discord chat * To get an emoji code, type your emoji in Discord chat with a backslash `\`
* with a backslash `\` in front. * in front. The bot has to be in the emoji's guild to use it.
* The bot has to be in the emoji's guild to use it.
*/ */
emoji?: string emoji?: string
} }

View File

@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import type { ComponentInteraction } from "../../internal/interaction" import type { ComponentInteraction } from "../../internal/interaction"
import type { MessageOptions } from "../../internal/message" import type { MessageOptions } from "../../internal/message"
@@ -8,30 +7,23 @@ import { Node } from "../../internal/node.js"
import type { ComponentEvent } from "../component-event" import type { ComponentEvent } from "../component-event"
import type { ButtonSharedProps } from "./button-shared-props" import type { ButtonSharedProps } from "./button-shared-props"
/** /** @category Button */
* @category Button
*/
export type ButtonProps = ButtonSharedProps & { export type ButtonProps = ButtonSharedProps & {
/** /**
* The style determines the color of the button and signals intent. * The style determines the color of the button and signals intent.
*
* @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles
*/ */
style?: "primary" | "secondary" | "success" | "danger" style?: "primary" | "secondary" | "success" | "danger"
/** /** Happens when a user clicks the button. */
* Happens when a user clicks the button.
*/
onClick: (event: ButtonClickEvent) => void onClick: (event: ButtonClickEvent) => void
} }
/** /** @category Button */
* @category Button
*/
export type ButtonClickEvent = ComponentEvent export type ButtonClickEvent = ComponentEvent
/** /** @category Button */
* @category Button
*/
export function Button(props: ButtonProps) { export function Button(props: ButtonProps) {
return ( return (
<ReacordElement props={props} createNode={() => new ButtonNode(props)}> <ReacordElement props={props} createNode={() => new ButtonNode(props)}>
@@ -46,7 +38,7 @@ class ButtonNode extends Node<ButtonProps> {
private customId = randomUUID() private customId = randomUUID()
// this has text children, but buttons themselves shouldn't yield text // this has text children, but buttons themselves shouldn't yield text
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line @typescript-eslint/class-literal-property-style
override get text() { override get text() {
return "" return ""
} }
@@ -74,4 +66,4 @@ class ButtonNode extends Node<ButtonProps> {
} }
} }
class ButtonLabelNode extends Node<{}> {} class ButtonLabelNode extends Node<Record<string, never>> {}

View File

@@ -1,23 +1,18 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { Node } from "../../internal/node.js" import { Node } from "../../internal/node.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
/** /** @category Embed */
* @category Embed export interface EmbedAuthorProps {
*/
export type EmbedAuthorProps = {
name?: ReactNode name?: ReactNode
children?: ReactNode children?: ReactNode
url?: string url?: string
iconUrl?: string iconUrl?: string
} }
/** /** @category Embed */
* @category Embed
*/
export function EmbedAuthor(props: EmbedAuthorProps) { export function EmbedAuthor(props: EmbedAuthorProps) {
return ( return (
<ReacordElement props={props} createNode={() => new EmbedAuthorNode(props)}> <ReacordElement props={props} createNode={() => new EmbedAuthorNode(props)}>
@@ -38,4 +33,4 @@ class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
} }
} }
class AuthorTextNode extends Node<{}> {} class AuthorTextNode extends Node<Record<string, never>> {}

View File

@@ -1,23 +1,18 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { Node } from "../../internal/node.js" import { Node } from "../../internal/node.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
/** /** @category Embed */
* @category Embed export interface EmbedFieldProps {
*/
export type EmbedFieldProps = {
name: ReactNode name: ReactNode
value?: ReactNode value?: ReactNode
inline?: boolean inline?: boolean
children?: ReactNode children?: ReactNode
} }
/** /** @category Embed */
* @category Embed
*/
export function EmbedField(props: EmbedFieldProps) { export function EmbedField(props: EmbedFieldProps) {
return ( return (
<ReacordElement props={props} createNode={() => new EmbedFieldNode(props)}> <ReacordElement props={props} createNode={() => new EmbedFieldNode(props)}>
@@ -25,7 +20,7 @@ export function EmbedField(props: EmbedFieldProps) {
{props.name} {props.name}
</ReacordElement> </ReacordElement>
<ReacordElement props={{}} createNode={() => new FieldValueNode({})}> <ReacordElement props={{}} createNode={() => new FieldValueNode({})}>
{props.value || props.children} {props.value ?? props.children}
</ReacordElement> </ReacordElement>
</ReacordElement> </ReacordElement>
) )
@@ -42,5 +37,5 @@ class EmbedFieldNode extends EmbedChildNode<EmbedFieldProps> {
} }
} }
class FieldNameNode extends Node<{}> {} class FieldNameNode extends Node<Record<string, never>> {}
class FieldValueNode extends Node<{}> {} class FieldValueNode extends Node<Record<string, never>> {}

View File

@@ -1,23 +1,18 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { Node } from "../../internal/node.js" import { Node } from "../../internal/node.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
/** /** @category Embed */
* @category Embed export interface EmbedFooterProps {
*/
export type EmbedFooterProps = {
text?: ReactNode text?: ReactNode
children?: ReactNode children?: ReactNode
iconUrl?: string iconUrl?: string
timestamp?: string | number | Date timestamp?: string | number | Date
} }
/** /** @category Embed */
* @category Embed
*/
export function EmbedFooter({ text, children, ...props }: EmbedFooterProps) { export function EmbedFooter({ text, children, ...props }: EmbedFooterProps) {
return ( return (
<ReacordElement props={props} createNode={() => new EmbedFooterNode(props)}> <ReacordElement props={props} createNode={() => new EmbedFooterNode(props)}>
@@ -42,4 +37,4 @@ class EmbedFooterNode extends EmbedChildNode<
} }
} }
class FooterTextNode extends Node<{}> {} class FooterTextNode extends Node<Record<string, never>> {}

View File

@@ -1,18 +1,13 @@
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
/** /** @category Embed */
* @category Embed export interface EmbedImageProps {
*/
export type EmbedImageProps = {
url: string url: string
} }
/** /** @category Embed */
* @category Embed
*/
export function EmbedImage(props: EmbedImageProps) { export function EmbedImage(props: EmbedImageProps) {
return ( return (
<ReacordElement <ReacordElement

View File

@@ -1,5 +1,5 @@
import type { Except, SnakeCasedPropertiesDeep } from "type-fest"
import type { EmbedProps } from "./embed" import type { EmbedProps } from "./embed"
import type { Except, SnakeCasedPropertiesDeep } from "type-fest"
export type EmbedOptions = SnakeCasedPropertiesDeep< export type EmbedOptions = SnakeCasedPropertiesDeep<
Except<EmbedProps, "timestamp" | "children"> & { Except<EmbedProps, "timestamp" | "children"> & {

View File

@@ -1,18 +1,13 @@
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
/** /** @category Embed */
* @category Embed export interface EmbedThumbnailProps {
*/
export type EmbedThumbnailProps = {
url: string url: string
} }
/** /** @category Embed */
* @category Embed
*/
export function EmbedThumbnail(props: EmbedThumbnailProps) { export function EmbedThumbnail(props: EmbedThumbnailProps) {
return ( return (
<ReacordElement <ReacordElement

View File

@@ -1,21 +1,16 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { Node } from "../../internal/node.js" import { Node } from "../../internal/node.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
/** /** @category Embed */
* @category Embed export interface EmbedTitleProps {
*/
export type EmbedTitleProps = {
children: ReactNode children: ReactNode
url?: string url?: string
} }
/** /** @category Embed */
* @category Embed
*/
export function EmbedTitle({ children, ...props }: EmbedTitleProps) { export function EmbedTitle({ children, ...props }: EmbedTitleProps) {
return ( return (
<ReacordElement props={props} createNode={() => new EmbedTitleNode(props)}> <ReacordElement props={props} createNode={() => new EmbedTitleNode(props)}>
@@ -33,4 +28,4 @@ class EmbedTitleNode extends EmbedChildNode<Omit<EmbedTitleProps, "children">> {
} }
} }
class TitleTextNode extends Node<{}> {} class TitleTextNode extends Node<Record<string, never>> {}

View File

@@ -1,6 +1,6 @@
import { snakeCaseDeep } from "@reacord/helpers/convert-object-property-case" import { snakeCaseDeep } from "@reacord/helpers/convert-object-property-case"
import { omit } from "@reacord/helpers/omit" import { omit } from "@reacord/helpers/omit"
import React from "react" import type React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import type { MessageOptions } from "../../internal/message" import type { MessageOptions } from "../../internal/message"
import { Node } from "../../internal/node.js" import { Node } from "../../internal/node.js"
@@ -12,7 +12,7 @@ import type { EmbedOptions } from "./embed-options"
* @category Embed * @category Embed
* @see https://discord.com/developers/docs/resources/channel#embed-object * @see https://discord.com/developers/docs/resources/channel#embed-object
*/ */
export type EmbedProps = { export interface EmbedProps {
title?: string title?: string
description?: string description?: string
url?: string url?: string
@@ -53,7 +53,7 @@ class EmbedNode extends Node<EmbedProps> {
child.modifyEmbedOptions(embed) child.modifyEmbedOptions(embed)
} }
if (child instanceof TextNode) { if (child instanceof TextNode) {
embed.description = (embed.description || "") + child.props embed.description = (embed.description ?? "") + child.props
} }
} }

View File

@@ -1,13 +1,10 @@
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import type { MessageOptions } from "../../internal/message" import type { MessageOptions } from "../../internal/message"
import { getNextActionRow } from "../../internal/message" import { getNextActionRow } from "../../internal/message"
import { Node } from "../../internal/node.js" import { Node } from "../../internal/node.js"
import type { ButtonSharedProps } from "./button-shared-props" import type { ButtonSharedProps } from "./button-shared-props"
/** /** @category Link */
* @category Link
*/
export type LinkProps = ButtonSharedProps & { export type LinkProps = ButtonSharedProps & {
/** The URL the link should lead to */ /** The URL the link should lead to */
url: string url: string
@@ -15,14 +12,12 @@ export type LinkProps = ButtonSharedProps & {
children?: string children?: string
} }
/** /** @category Link */
* @category Link
*/
export function Link({ label, children, ...props }: LinkProps) { export function Link({ label, children, ...props }: LinkProps) {
return ( return (
<ReacordElement props={props} createNode={() => new LinkNode(props)}> <ReacordElement props={props} createNode={() => new LinkNode(props)}>
<ReacordElement props={{}} createNode={() => new LinkTextNode({})}> <ReacordElement props={{}} createNode={() => new LinkTextNode({})}>
{label || children} {label ?? children}
</ReacordElement> </ReacordElement>
</ReacordElement> </ReacordElement>
) )
@@ -40,4 +35,4 @@ class LinkNode extends Node<Omit<LinkProps, "label" | "children">> {
} }
} }
class LinkTextNode extends Node<{}> {} class LinkTextNode extends Node<Record<string, never>> {}

View File

@@ -15,5 +15,5 @@ export class OptionNode extends Node<
} }
} }
export class OptionLabelNode extends Node<{}> {} export class OptionLabelNode extends Node<Record<string, never>> {}
export class OptionDescriptionNode extends Node<{}> {} export class OptionDescriptionNode extends Node<Record<string, never>> {}

View File

@@ -1,5 +1,4 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react"
import { ReacordElement } from "../../internal/element" import { ReacordElement } from "../../internal/element"
import { import {
OptionDescriptionNode, OptionDescriptionNode,
@@ -7,10 +6,8 @@ import {
OptionNode, OptionNode,
} from "./option-node" } from "./option-node"
/** /** @category Select */
* @category Select export interface OptionProps {
*/
export type OptionProps = {
/** The internal value of this option */ /** The internal value of this option */
value: string value: string
/** The text shown to the user. This takes priority over `children` */ /** The text shown to the user. This takes priority over `children` */
@@ -23,19 +20,16 @@ export type OptionProps = {
/** /**
* Renders an emoji to the left of the text. * Renders an emoji to the left of the text.
* *
* Has to be a literal emoji character (e.g. 🍍), * Has to be a literal emoji character (e.g. 🍍), or an emoji code, like
* or an emoji code, like `<:plus_one:778531744860602388>`. * `<:plus_one:778531744860602388>`.
* *
* To get an emoji code, type your emoji in Discord chat * To get an emoji code, type your emoji in Discord chat with a backslash `\`
* with a backslash `\` in front. * in front. The bot has to be in the emoji's guild to use it.
* The bot has to be in the emoji's guild to use it.
*/ */
emoji?: string emoji?: string
} }
/** /** @category Select */
* @category Select
*/
export function Option({ export function Option({
label, label,
children, children,
@@ -46,7 +40,7 @@ export function Option({
<ReacordElement props={props} createNode={() => new OptionNode(props)}> <ReacordElement props={props} createNode={() => new OptionNode(props)}>
{(label !== undefined || children !== undefined) && ( {(label !== undefined || children !== undefined) && (
<ReacordElement props={{}} createNode={() => new OptionLabelNode({})}> <ReacordElement props={{}} createNode={() => new OptionLabelNode({})}>
{label || children} {label ?? children}
</ReacordElement> </ReacordElement>
)} )}
{description !== undefined && ( {description !== undefined && (

View File

@@ -1,7 +1,6 @@
import { isInstanceOf } from "@reacord/helpers/is-instance-of" import { isInstanceOf } from "@reacord/helpers/is-instance-of"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import type { ComponentInteraction } from "../../internal/interaction" import type { ComponentInteraction } from "../../internal/interaction"
import type { import type {
@@ -12,11 +11,10 @@ import type {
import { Node } from "../../internal/node.js" import { Node } from "../../internal/node.js"
import type { ComponentEvent } from "../component-event" import type { ComponentEvent } from "../component-event"
import { OptionNode } from "./option-node" import { OptionNode } from "./option-node"
import { omit } from "@reacord/helpers/omit.js"
/** /** @category Select */
* @category Select export interface SelectProps {
*/
export type SelectProps = {
children?: ReactNode children?: ReactNode
/** Sets the currently selected value */ /** Sets the currently selected value */
value?: string value?: string
@@ -31,8 +29,8 @@ export type SelectProps = {
multiple?: boolean multiple?: boolean
/** /**
* With `multiple`, the minimum number of values that can be selected. * With `multiple`, the minimum number of values that can be selected. When
* When `multiple` is false or not defined, this is always 1. * `multiple` is false or not defined, this is always 1.
* *
* This only limits the number of values that can be received by the user. * This only limits the number of values that can be received by the user.
* This does not limit the number of values that can be displayed by you. * This does not limit the number of values that can be displayed by you.
@@ -40,44 +38,44 @@ export type SelectProps = {
minValues?: number minValues?: number
/** /**
* With `multiple`, the maximum number of values that can be selected. * With `multiple`, the maximum number of values that can be selected. When
* When `multiple` is false or not defined, this is always 1. * `multiple` is false or not defined, this is always 1.
* *
* This only limits the number of values that can be received by the user. * This only limits the number of values that can be received by the user.
* This does not limit the number of values that can be displayed by you. * This does not limit the number of values that can be displayed by you.
*/ */
maxValues?: number maxValues?: number
/** When true, the select will be slightly faded, and cannot be interacted with. */ /**
* When true, the select will be slightly faded, and cannot be interacted
* with.
*/
disabled?: boolean disabled?: boolean
/** /**
* Called when the user inputs a selection. * Called when the user inputs a selection. Receives the entire select change
* Receives the entire select change event, * event, which can be used to create new replies, etc.
* which can be used to create new replies, etc.
*/ */
onChange?: (event: SelectChangeEvent) => void onChange?: (event: SelectChangeEvent) => void
/** /**
* Convenience shorthand for `onChange`, which receives the first selected value. * Convenience shorthand for `onChange`, which receives the first selected
* value.
*/ */
onChangeValue?: (value: string, event: SelectChangeEvent) => void onChangeValue?: (value: string, event: SelectChangeEvent) => void
/** /** Convenience shorthand for `onChange`, which receives all selected values. */
* Convenience shorthand for `onChange`, which receives all selected values.
*/
onChangeMultiple?: (values: string[], event: SelectChangeEvent) => void onChangeMultiple?: (values: string[], event: SelectChangeEvent) => void
} }
/** /** @category Select */
* @category Select
*/
export type SelectChangeEvent = ComponentEvent & { export type SelectChangeEvent = ComponentEvent & {
values: string[] values: string[]
} }
/** /**
* See [the select menu guide](/guides/select-menu) for a usage example. * See [the select menu guide](/guides/select-menu) for a usage example.
*
* @category Select * @category Select
*/ */
export function Select(props: SelectProps) { export function Select(props: SelectProps) {
@@ -105,12 +103,13 @@ class SelectNode extends Node<SelectProps> {
values, values,
minValues = 0, minValues = 0,
maxValues = 25, maxValues = 25,
children,
onChange,
onChangeValue,
onChangeMultiple,
...props ...props
} = this.props } = omit(this.props, [
"children",
"onChange",
"onChangeValue",
"onChangeMultiple",
])
const item: ActionRowItem = { const item: ActionRowItem = {
...props, ...props,

View File

@@ -1,6 +1,6 @@
import { raise } from "@reacord/helpers/raise" import type { ReacordInstance } from "./instance.js"
import { raise } from "@reacord/helpers/raise.js"
import * as React from "react" import * as React from "react"
import type { ReacordInstance } from "./instance"
const Context = React.createContext<ReacordInstance | undefined>(undefined) const Context = React.createContext<ReacordInstance | undefined>(undefined)

View File

@@ -2,9 +2,10 @@ import type { ReactNode } from "react"
/** /**
* Represents an interactive message, which can later be replaced or deleted. * Represents an interactive message, which can later be replaced or deleted.
*
* @category Core * @category Core
*/ */
export type ReacordInstance = { export interface ReacordInstance {
/** Render some JSX to this instance (edits the message) */ /** Render some JSX to this instance (edits the message) */
render: (content: ReactNode) => void render: (content: ReactNode) => void

View File

@@ -1,4 +1,4 @@
/* eslint-disable class-methods-use-this */ import { safeJsonStringify } from "@reacord/helpers/json"
import { pick } from "@reacord/helpers/pick" import { pick } from "@reacord/helpers/pick"
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values" import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
import { raise } from "@reacord/helpers/raise" import { raise } from "@reacord/helpers/raise"
@@ -26,14 +26,18 @@ import { Reacord } from "./reacord"
/** /**
* The Reacord adapter for Discord.js. * The Reacord adapter for Discord.js.
*
* @category Core * @category Core
*/ */
export class ReacordDiscordJs extends Reacord { export class ReacordDiscordJs extends Reacord {
constructor(private client: Discord.Client, config: ReacordConfig = {}) { constructor(
private client: Discord.Client,
config: ReacordConfig = {},
) {
super(config) super(config)
client.on("interactionCreate", (interaction) => { client.on("interactionCreate", (interaction) => {
if (interaction.isButton() || interaction.isSelectMenu()) { if (interaction.isButton() || interaction.isStringSelectMenu()) {
this.handleComponentInteraction( this.handleComponentInteraction(
this.createReacordComponentInteraction(interaction), this.createReacordComponentInteraction(interaction),
) )
@@ -43,6 +47,7 @@ export class ReacordDiscordJs extends Reacord {
/** /**
* Sends a message to a channel. * Sends a message to a channel.
*
* @see https://reacord.mapleleaf.dev/guides/sending-messages * @see https://reacord.mapleleaf.dev/guides/sending-messages
*/ */
override send( override send(
@@ -57,6 +62,7 @@ export class ReacordDiscordJs extends Reacord {
/** /**
* Sends a message as a reply to a command interaction. * Sends a message as a reply to a command interaction.
*
* @see https://reacord.mapleleaf.dev/guides/sending-messages * @see https://reacord.mapleleaf.dev/guides/sending-messages
*/ */
override reply( override reply(
@@ -71,6 +77,7 @@ export class ReacordDiscordJs extends Reacord {
/** /**
* Sends an ephemeral message as a reply to a command interaction. * Sends an ephemeral message as a reply to a command interaction.
*
* @see https://reacord.mapleleaf.dev/guides/sending-messages * @see https://reacord.mapleleaf.dev/guides/sending-messages
*/ */
override ephemeralReply( override ephemeralReply(
@@ -114,14 +121,14 @@ export class ReacordDiscordJs extends Reacord {
...getDiscordMessageOptions(options), ...getDiscordMessageOptions(options),
fetchReply: true, fetchReply: true,
}) })
return createReacordMessage(message as Discord.Message) return createReacordMessage(message)
}, },
followUp: async (options) => { followUp: async (options) => {
const message = await interaction.followUp({ const message = await interaction.followUp({
...getDiscordMessageOptions(options), ...getDiscordMessageOptions(options),
fetchReply: true, fetchReply: true,
}) })
return createReacordMessage(message as Discord.Message) return createReacordMessage(message)
}, },
}) })
} }
@@ -189,6 +196,8 @@ export class ReacordDiscordJs extends Reacord {
? new Date(interaction.message.editedTimestamp).toISOString() ? new Date(interaction.message.editedTimestamp).toISOString()
: undefined, : undefined,
mentions: interaction.message.mentions.users.map((u) => u.id), mentions: interaction.message.mentions.users.map((u) => u.id),
authorId: interaction.message.author.id,
mentionEveryone: interaction.message.mentions.everyone,
} }
: raise("Message not found") : raise("Message not found")
@@ -212,6 +221,8 @@ export class ReacordDiscordJs extends Reacord {
premiumSince: interaction.member.premiumSince?.toISOString(), premiumSince: interaction.member.premiumSince?.toISOString(),
communicationDisabledUntil: communicationDisabledUntil:
interaction.member.communicationDisabledUntil?.toISOString(), interaction.member.communicationDisabledUntil?.toISOString(),
color: interaction.member.displayColor,
displayAvatarUrl: interaction.member.displayAvatarURL(),
} }
: undefined : undefined
@@ -226,7 +237,7 @@ export class ReacordDiscordJs extends Reacord {
...pruneNullishValues( ...pruneNullishValues(
pick(interaction.user, ["id", "username", "discriminator", "tag"]), pick(interaction.user, ["id", "username", "discriminator", "tag"]),
), ),
avatarUrl: interaction.user.avatarURL()!, avatarUrl: interaction.user.avatarURL(),
accentColor: interaction.user.accentColor ?? undefined, accentColor: interaction.user.accentColor ?? undefined,
} }
@@ -234,7 +245,11 @@ export class ReacordDiscordJs extends Reacord {
id: interaction.id, id: interaction.id,
customId: interaction.customId, customId: interaction.customId,
update: async (options: MessageOptions) => { update: async (options: MessageOptions) => {
if (interaction.deferred || interaction.replied) {
await interaction.message.edit(getDiscordMessageOptions(options))
} else {
await interaction.update(getDiscordMessageOptions(options)) await interaction.update(getDiscordMessageOptions(options))
}
}, },
deferUpdate: async () => { deferUpdate: async () => {
if (interaction.replied || interaction.deferred) return if (interaction.replied || interaction.deferred) return
@@ -245,14 +260,14 @@ export class ReacordDiscordJs extends Reacord {
...getDiscordMessageOptions(options), ...getDiscordMessageOptions(options),
fetchReply: true, fetchReply: true,
}) })
return createReacordMessage(message as Discord.Message) return createReacordMessage(message)
}, },
followUp: async (options) => { followUp: async (options) => {
const message = await interaction.followUp({ const message = await interaction.followUp({
...getDiscordMessageOptions(options), ...getDiscordMessageOptions(options),
fetchReply: true, fetchReply: true,
}) })
return createReacordMessage(message as Discord.Message) return createReacordMessage(message)
}, },
event: { event: {
channel, channel,
@@ -281,7 +296,7 @@ export class ReacordDiscordJs extends Reacord {
} }
} }
if (interaction.isSelectMenu()) { if (interaction.isStringSelectMenu()) {
return { return {
...baseProps, ...baseProps,
type: "select", type: "select",
@@ -335,8 +350,7 @@ function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
// and also handle some edge cases, e.g. empty messages // and also handle some edge cases, e.g. empty messages
function getDiscordMessageOptions(reacordOptions: MessageOptions) { function getDiscordMessageOptions(reacordOptions: MessageOptions) {
const options = { const options = {
// eslint-disable-next-line unicorn/no-null content: reacordOptions.content || undefined,
content: reacordOptions.content || null,
embeds: reacordOptions.embeds, embeds: reacordOptions.embeds,
components: reacordOptions.actionRows.map((row) => ({ components: reacordOptions.actionRows.map((row) => ({
type: Discord.ComponentType.ActionRow, type: Discord.ComponentType.ActionRow,
@@ -364,6 +378,8 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
} }
} }
// future proofing
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (component.type === "select") { if (component.type === "select") {
return { return {
...component, ...component,
@@ -375,13 +391,16 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
} }
} }
raise(`Unsupported component type: ${(component as any).type}`) component satisfies never
throw new Error(
`Invalid component type ${safeJsonStringify(component)}}`,
)
}, },
), ),
})), })),
} }
if (!options.content && !options.embeds?.length) { if (!options.content && !options.embeds.length) {
options.content = "_ _" options.content = "_ _"
} }

View File

@@ -1,25 +1,22 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react" import type { ComponentInteraction } from "../internal/interaction.js"
import type { ComponentInteraction } from "../internal/interaction"
import { reconciler } from "../internal/reconciler.js" import { reconciler } from "../internal/reconciler.js"
import type { Renderer } from "../internal/renderers/renderer" import type { Renderer } from "../internal/renderers/renderer.js"
import type { ReacordInstance } from "./instance" import { InstanceProvider } from "./instance-context.js"
import { InstanceProvider } from "./instance-context" import type { ReacordInstance } from "./instance.js"
/** /** @category Core */
* @category Core export interface ReacordConfig {
*/
export type ReacordConfig = {
/** /**
* The max number of active instances. * The max number of active instances. When this limit is exceeded, the oldest
* When this limit is exceeded, the oldest instances will be disabled. * instances will be disabled.
*/ */
maxInstances?: number maxInstances?: number
} }
/** /**
* The main Reacord class that other Reacord adapters should extend. * The main Reacord class that other Reacord adapters should extend. Only use
* Only use this directly if you're making [a custom adapter](/guides/custom-adapters). * this directly if you're making [a custom adapter](/guides/custom-adapters).
*/ */
export abstract class Reacord { export abstract class Reacord {
private renderers: Renderer[] = [] private renderers: Renderer[] = []
@@ -41,23 +38,20 @@ export abstract class Reacord {
} }
protected createInstance(renderer: Renderer, initialContent?: ReactNode) { protected createInstance(renderer: Renderer, initialContent?: ReactNode) {
if (this.renderers.length > this.maxInstances) { if (this.renderers.length > this.maxInstances && this.renderers[0]) {
this.deactivate(this.renderers[0]!) this.deactivate(this.renderers[0])
} }
this.renderers.push(renderer) this.renderers.push(renderer)
const container = reconciler.createContainer( const container: unknown = reconciler.createContainer(
renderer, renderer,
0, 0,
// eslint-disable-next-line unicorn/no-null
null, null,
false, false,
// eslint-disable-next-line unicorn/no-null
null, null,
"reacord", "reacord",
() => {}, () => {},
// eslint-disable-next-line unicorn/no-null
null, null,
) )

View File

@@ -1,5 +1,5 @@
import type { Message, MessageOptions } from "./message" import type { Message, MessageOptions } from "./message"
export type Channel = { export interface Channel {
send(message: MessageOptions): Promise<Message> send(message: MessageOptions): Promise<Message>
} }

View File

@@ -25,7 +25,9 @@ export class Container<T> {
return this.items.find(predicate) return this.items.find(predicate)
} }
findType<U extends T>(type: new (...args: any[]) => U): U | undefined { findType<U extends T>(
type: new (...args: Array<NonNullable<unknown>>) => U,
): U | undefined {
for (const item of this.items) { for (const item of this.items) {
if (item instanceof type) return item if (item instanceof type) return item
} }

View File

@@ -1,6 +1,6 @@
import type { Node } from "./node"
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react" import React from "react"
import type { Node } from "./node"
export function ReacordElement<Props>(props: { export function ReacordElement<Props>(props: {
props: Props props: Props

View File

@@ -17,7 +17,7 @@ export type SelectInteraction = BaseComponentInteraction<
SelectChangeEvent SelectChangeEvent
> >
export type BaseInteraction<Type extends string> = { export interface BaseInteraction<Type extends string> {
type: Type type: Type
id: string id: string
reply(messageOptions: MessageOptions): Promise<Message> reply(messageOptions: MessageOptions): Promise<Message>

View File

@@ -1,9 +1,9 @@
import { last } from "@reacord/helpers/last"
import type { Except } from "type-fest"
import type { EmbedOptions } from "../core/components/embed-options" import type { EmbedOptions } from "../core/components/embed-options"
import type { SelectProps } from "../core/components/select" import type { SelectProps } from "../core/components/select"
import { last } from "@reacord/helpers/last"
import type { Except } from "type-fest"
export type MessageOptions = { export interface MessageOptions {
content: string content: string
embeds: EmbedOptions[] embeds: EmbedOptions[]
actionRows: ActionRow[] actionRows: ActionRow[]
@@ -16,7 +16,7 @@ export type ActionRowItem =
| MessageLinkOptions | MessageLinkOptions
| MessageSelectOptions | MessageSelectOptions
export type MessageButtonOptions = { export interface MessageButtonOptions {
type: "button" type: "button"
customId: string customId: string
label?: string label?: string
@@ -25,7 +25,7 @@ export type MessageButtonOptions = {
emoji?: string emoji?: string
} }
export type MessageLinkOptions = { export interface MessageLinkOptions {
type: "link" type: "link"
url: string url: string
label?: string label?: string
@@ -39,14 +39,14 @@ export type MessageSelectOptions = Except<SelectProps, "children" | "value"> & {
options: MessageSelectOptionOptions[] options: MessageSelectOptionOptions[]
} }
export type MessageSelectOptionOptions = { export interface MessageSelectOptionOptions {
label: string label: string
value: string value: string
description?: string description?: string
emoji?: string emoji?: string
} }
export type Message = { export interface Message {
edit(options: MessageOptions): Promise<void> edit(options: MessageOptions): Promise<void>
delete(): Promise<void> delete(): Promise<void>
} }

View File

@@ -1,4 +1,3 @@
/* eslint-disable class-methods-use-this */
import { Container } from "./container.js" import { Container } from "./container.js"
import type { ComponentInteraction } from "./interaction" import type { ComponentInteraction } from "./interaction"
import type { MessageOptions } from "./message" import type { MessageOptions } from "./message"
@@ -8,9 +7,11 @@ export abstract class Node<Props> {
constructor(public props: Props) {} constructor(public props: Props) {}
modifyMessageOptions(options: MessageOptions) {} modifyMessageOptions(_options: MessageOptions) {
// noop
}
handleComponentInteraction(interaction: ComponentInteraction): boolean { handleComponentInteraction(_interaction: ComponentInteraction): boolean {
return false return false
} }

View File

@@ -29,7 +29,6 @@ const config: HostConfig<
cancelTimeout: global.clearTimeout, cancelTimeout: global.clearTimeout,
noTimeout: -1, noTimeout: -1,
// eslint-disable-next-line unicorn/no-null
getRootHostContext: () => null, getRootHostContext: () => null,
getChildHostContext: (parentContext) => parentContext, getChildHostContext: (parentContext) => parentContext,
@@ -42,7 +41,7 @@ const config: HostConfig<
raise(`Missing createNode function`) raise(`Missing createNode function`)
} }
const node = props.createNode(props.props) const node: unknown = props.createNode(props.props)
if (!(node instanceof Node)) { if (!(node instanceof Node)) {
raise(`createNode function did not return a Node`) raise(`createNode function did not return a Node`)
} }
@@ -51,13 +50,11 @@ const config: HostConfig<
}, },
createTextInstance: (text) => new TextNode(text), createTextInstance: (text) => new TextNode(text),
shouldSetTextContent: () => false, shouldSetTextContent: () => false,
detachDeletedInstance: (instance) => {}, detachDeletedInstance: (_instance) => {},
beforeActiveInstanceBlur: () => {}, beforeActiveInstanceBlur: () => {},
afterActiveInstanceBlur: () => {}, afterActiveInstanceBlur: () => {},
// eslint-disable-next-line unicorn/no-null getInstanceFromNode: (_node: unknown) => null,
getInstanceFromNode: (node: any) => null, getInstanceFromScope: (_scopeInstance: unknown) => null,
// eslint-disable-next-line unicorn/no-null
getInstanceFromScope: (scopeInstance: any) => null,
clearContainer: (renderer) => { clearContainer: (renderer) => {
renderer.nodes.clear() renderer.nodes.clear()
@@ -93,12 +90,11 @@ const config: HostConfig<
node.props = newText node.props = newText
}, },
// eslint-disable-next-line unicorn/no-null
prepareForCommit: () => null, prepareForCommit: () => null,
resetAfterCommit: (renderer) => { resetAfterCommit: (renderer) => {
renderer.render() renderer.render()
}, },
prepareScopeUpdate: (scopeInstance: any, instance: any) => {}, prepareScopeUpdate: (_scopeInstance: unknown, _instance: unknown) => {},
preparePortalMount: () => raise("Portals are not supported"), preparePortalMount: () => raise("Portals are not supported"),
getPublicInstance: () => raise("Refs are currently not supported"), getPublicInstance: () => raise("Refs are currently not supported"),

View File

@@ -1,9 +1,9 @@
import { Subject } from "rxjs"
import { concatMap } from "rxjs/operators"
import { Container } from "../container.js" import { Container } from "../container.js"
import type { ComponentInteraction } from "../interaction" import type { ComponentInteraction } from "../interaction"
import type { Message, MessageOptions } from "../message" import type { Message, MessageOptions } from "../message"
import type { Node } from "../node.js" import type { Node } from "../node.js"
import { Subject } from "rxjs"
import { concatMap } from "rxjs/operators"
type UpdatePayload = type UpdatePayload =
| { action: "update" | "deactivate"; options: MessageOptions } | { action: "update" | "deactivate"; options: MessageOptions }
@@ -47,14 +47,12 @@ export abstract class Renderer {
} }
handleComponentInteraction(interaction: ComponentInteraction) { handleComponentInteraction(interaction: ComponentInteraction) {
for (const node of this.nodes) {
if (node.handleComponentInteraction(interaction)) {
this.componentInteraction = interaction this.componentInteraction = interaction
setTimeout(() => { setTimeout(() => {
this.updates.next({ action: "deferUpdate", interaction }) this.updates.next({ action: "deferUpdate", interaction })
}, 500) }, 500)
for (const node of this.nodes) {
if (node.handleComponentInteraction(interaction)) {
return true return true
} }
} }

View File

@@ -2,7 +2,7 @@
"name": "reacord", "name": "reacord",
"type": "module", "type": "module",
"description": "Create interactive Discord messages using React.", "description": "Create interactive Discord messages using React.",
"version": "0.5.2", "version": "0.5.3",
"types": "./dist/main.d.ts", "types": "./dist/main.d.ts",
"homepage": "https://reacord.mapleleaf.dev", "homepage": "https://reacord.mapleleaf.dev",
"repository": "https://github.com/itsMapleLeaf/reacord.git", "repository": "https://github.com/itsMapleLeaf/reacord.git",
@@ -20,6 +20,7 @@
"reacord" "reacord"
], ],
"files": [ "files": [
"library",
"dist", "dist",
"README.md", "README.md",
"LICENSE" "LICENSE"
@@ -28,7 +29,7 @@
".": { ".": {
"import": "./dist/main.js", "import": "./dist/main.js",
"require": "./dist/main.cjs", "require": "./dist/main.cjs",
"types": "./dist/main.d.ts" "types": "./library/main.ts"
}, },
"./package.json": { "./package.json": {
"import": "./package.json", "import": "./package.json",
@@ -36,19 +37,19 @@
} }
}, },
"scripts": { "scripts": {
"build": "cp ../../README.md . && cp ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --dts --sourcemap", "build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --sourcemap",
"build-watch": "pnpm build -- --watch", "build-watch": "pnpm build -- --watch",
"test": "vitest --coverage --no-watch", "test": "vitest --coverage --no-watch",
"test-dev": "vitest", "test-dev": "vitest",
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx", "test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
"typecheck": "tsc --noEmit" "typecheck": "tsc -b"
}, },
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "^20.7.0",
"@types/react": "*", "@types/react": "^18.2.23",
"@types/react-reconciler": "^0.28.0", "@types/react-reconciler": "^0.28.5",
"react-reconciler": "^0.29.0", "react-reconciler": "^0.29.0",
"rxjs": "^7.5.6" "rxjs": "^7.8.1"
}, },
"peerDependencies": { "peerDependencies": {
"discord.js": "^14", "discord.js": "^14",
@@ -61,23 +62,19 @@
}, },
"devDependencies": { "devDependencies": {
"@reacord/helpers": "workspace:*", "@reacord/helpers": "workspace:*",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.9",
"c8": "^7.12.0", "c8": "^8.0.1",
"discord.js": "^14.0.3", "cpy-cli": "^5.0.0",
"dotenv": "^16.0.1", "discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nodemon": "^2.0.19", "nodemon": "^3.0.1",
"prettier": "^2.7.1", "prettier": "^3.0.3",
"pretty-ms": "^8.0.0", "pretty-ms": "^8.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"tsup": "^6.1.3", "tsup": "^7.2.0",
"tsx": "^3.8.0", "tsx": "^3.13.0",
"type-fest": "^2.17.0", "type-fest": "^4.3.2"
"typescript": "^4.7.4",
"vitest": "^0.18.1"
},
"resolutions": {
"esbuild": "latest"
}, },
"release-it": { "release-it": {
"git": { "git": {

View File

@@ -1,9 +1,4 @@
import type { TextChannel } from "discord.js" import { raise } from "@reacord/helpers/raise.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"
import { import {
Button, Button,
Link, Link,
@@ -11,16 +6,26 @@ import {
ReacordDiscordJs, ReacordDiscordJs,
Select, Select,
useInstance, useInstance,
} from "../library/main" } from "../library/main.js"
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 }) const client = new Client({ intents: IntentsBitField.Flags.Guilds })
const reacord = new ReacordDiscordJs(client) const reacord = new ReacordDiscordJs(client)
await client.login(process.env.TEST_BOT_TOKEN) await client.login(process.env.TEST_BOT_TOKEN)
const guild = await client.guilds.fetch(process.env.TEST_GUILD_ID!) const guild = await client.guilds.fetch(
process.env.TEST_GUILD_ID ?? raise("TEST_GUILD_ID not defined"),
)
const category = await guild.channels.fetch(process.env.TEST_CATEGORY_ID!) const category = await guild.channels.fetch(
process.env.TEST_CATEGORY_ID ?? raise("TEST_CATEGORY_ID not defined"),
)
if (category?.type !== ChannelType.GuildCategory) { if (category?.type !== ChannelType.GuildCategory) {
throw new Error( throw new Error(
`channel ${process.env.TEST_CATEGORY_ID} is not a guild category. received ${category?.type}`, `channel ${process.env.TEST_CATEGORY_ID} is not a guild category. received ${category?.type}`,
@@ -34,7 +39,7 @@ for (const [, channel] of category.children.cache) {
let prefix = 0 let prefix = 0
const createTest = async ( const createTest = async (
name: string, name: string,
block: (channel: TextChannel) => void | Promise<unknown>, block: (channel: TextChannel) => unknown,
) => { ) => {
prefix += 1 prefix += 1
const channel = await category.children.create({ const channel = await category.children.create({

View File

@@ -1,4 +1,3 @@
import React from "react"
import { test } from "vitest" import { test } from "vitest"
import { ActionRow, Button, Select } from "../library/main" import { ActionRow, Button, Select } from "../library/main"
import { ReacordTester } from "./test-adapter" import { ReacordTester } from "./test-adapter"

View File

@@ -8,5 +8,5 @@ beforeAll(() => {
test("can require commonjs", () => { test("can require commonjs", () => {
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
expect(() => require("../dist/main.cjs")).not.toThrow() expect(() => require("../dist/main.cjs") as unknown).not.toThrow()
}) })

View File

@@ -1,4 +1,3 @@
import React from "react"
import { test } from "vitest" import { test } from "vitest"
import { import {
Embed, Embed,

View File

@@ -1,2 +1,3 @@
import { test } from "vitest" import { test } from "vitest"
test.todo("ephemeral reply") test.todo("ephemeral reply")

View File

@@ -1,4 +1,3 @@
import React from "react"
import { test } from "vitest" import { test } from "vitest"
import { Link } from "../library/main" import { Link } from "../library/main"
import { ReacordTester } from "./test-adapter" import { ReacordTester } from "./test-adapter"

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import { test } from "vitest"
import { Button, Embed, EmbedField, EmbedTitle } from "../library/main" import { Button, Embed, EmbedField, EmbedTitle } from "../library/main"
import { ReacordTester } from "./test-adapter" import { ReacordTester } from "./test-adapter"
import * as React from "react"
import { test } from "vitest"
test("rendering behavior", async () => { test("rendering behavior", async () => {
const tester = new ReacordTester() const tester = new ReacordTester()

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react" import { useState } from "react"
import { expect, test, vi } from "vitest" import { expect, test, vi } from "vitest"
import { Button, Option, Select } from "../library/main" import { Button, Option, Select } from "../library/main"
import { ReacordTester } from "./test-adapter" import { ReacordTester } from "./test-adapter"
@@ -128,13 +128,17 @@ test("multiple select", async () => {
await tester.findSelectByPlaceholder("select").select("1", "3") await tester.findSelectByPlaceholder("select").select("1", "3")
await assertSelect(expect.arrayContaining(["1", "3"]) as unknown as string[]) await assertSelect(expect.arrayContaining(["1", "3"]) as unknown as string[])
expect(onSelect).toHaveBeenCalledWith( expect(onSelect).toHaveBeenCalledWith(
expect.objectContaining({ values: expect.arrayContaining(["1", "3"]) }), expect.objectContaining({
values: expect.arrayContaining(["1", "3"]) as unknown,
}),
) )
await tester.findSelectByPlaceholder("select").select("2") await tester.findSelectByPlaceholder("select").select("2")
await assertSelect(expect.arrayContaining(["2"]) as unknown as string[]) await assertSelect(expect.arrayContaining(["2"]) as unknown as string[])
expect(onSelect).toHaveBeenCalledWith( expect(onSelect).toHaveBeenCalledWith(
expect.objectContaining({ values: expect.arrayContaining(["2"]) }), expect.objectContaining({
values: expect.arrayContaining(["2"]) as unknown,
}),
) )
await tester.findSelectByPlaceholder("select").select() await tester.findSelectByPlaceholder("select").select()

View File

@@ -1,5 +1,3 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable require-await */
import { logPretty } from "@reacord/helpers/log-pretty" import { logPretty } from "@reacord/helpers/log-pretty"
import { omit } from "@reacord/helpers/omit" import { omit } from "@reacord/helpers/omit"
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values" import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
@@ -32,9 +30,7 @@ import { InteractionReplyRenderer } from "../library/internal/renderers/interact
export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0] export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0]
/** /** A Record adapter for automated tests. WIP */
* A Record adapter for automated tests. WIP
*/
export class ReacordTester extends Reacord { export class ReacordTester extends Reacord {
private messageContainer = new Container<TestMessage>() private messageContainer = new Container<TestMessage>()
@@ -252,10 +248,10 @@ class TestSelectInteraction
class TestComponentEvent { class TestComponentEvent {
constructor(private tester: ReacordTester) {} constructor(private tester: ReacordTester) {}
message: MessageInfo = {} as any // todo message: MessageInfo = {} as MessageInfo // todo
channel: ChannelInfo = {} as any // todo channel: ChannelInfo = {} as ChannelInfo // todo
user: UserInfo = {} as any // todo user: UserInfo = {} as UserInfo // todo
guild: GuildInfo = {} as any // todo guild: GuildInfo = {} as GuildInfo // todo
reply(content?: ReactNode): ReacordInstance { reply(content?: ReactNode): ReacordInstance {
return this.tester.reply(content) return this.tester.reply(content)
@@ -274,7 +270,10 @@ class TestSelectChangeEvent
extends TestComponentEvent extends TestComponentEvent
implements SelectChangeEvent implements SelectChangeEvent
{ {
constructor(readonly values: string[], tester: ReacordTester) { constructor(
readonly values: string[],
tester: ReacordTester,
) {
super(tester) super(tester)
} }
} }

View File

@@ -1,4 +1,3 @@
import * as React from "react"
import { test } from "vitest" import { test } from "vitest"
import { import {
Button, Button,

View File

@@ -1,9 +1,8 @@
import React from "react"
import { describe, expect, it } from "vitest"
import type { ReacordInstance } from "../library/main" import type { ReacordInstance } from "../library/main"
import { Button, useInstance } from "../library/main" import { Button, useInstance } from "../library/main"
import type { MessageSample } from "./test-adapter" import type { MessageSample } from "./test-adapter"
import { ReacordTester } from "./test-adapter" import { ReacordTester } from "./test-adapter"
import { describe, expect, it } from "vitest"
describe("useInstance", () => { describe("useInstance", () => {
it("returns the instance of itself", async () => { it("returns the instance of itself", async () => {

View File

@@ -1,4 +1,7 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"include": ["**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"] "compilerOptions": {
"jsx": "react-jsx"
},
"exclude": ["node_modules", "dist"]
} }

View File

@@ -1,5 +1,15 @@
# website # website
## 0.4.4
### Patch Changes
- Updated dependencies [104b175]
- Updated dependencies [156cf90]
- Updated dependencies [0bab505]
- Updated dependencies [d76f316]
- reacord@0.5.3
## 0.4.3 ## 0.4.3
### Patch Changes ### Patch Changes

View File

@@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import prefetch from "@astrojs/prefetch" import prefetch from "@astrojs/prefetch"
import react from "@astrojs/react" import react from "@astrojs/react"
import tailwind from "@astrojs/tailwind" import tailwind from "@astrojs/tailwind"
@@ -7,11 +9,12 @@ import { defineConfig } from "astro/config"
export default defineConfig({ export default defineConfig({
integrations: [ integrations: [
tailwind({ tailwind({
config: {
applyBaseStyles: false, applyBaseStyles: false,
},
}), }),
react(), react(),
prefetch(), prefetch(),
], ],
markdown: {
shikiConfig: {},
},
}) })

View File

@@ -1,42 +1,41 @@
{ {
"type": "module", "type": "module",
"name": "website", "name": "website",
"version": "0.4.3", "version": "0.4.4",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"dev": "run-p --race --print-label dev:*", "dev": "run-p --race --print-label dev:*",
"dev:typedoc": "typedoc --watch", "dev:typedoc": "typedoc --watch",
"dev:astro": "astro dev", "dev:astro": "astro dev",
"test": "node ./scripts/test.js",
"test-dev": "run-p --race --print-label dev:* test-dev:*",
"test-dev:cypress": "wait-on http-get://localhost:3000 && cypress open",
"start": "astro preview", "start": "astro preview",
"build": "typedoc && astro build", "build": "typedoc && astro build",
"typecheck": "tsc --noEmit && tsc --project cypress/tsconfig.json --noEmit" "typecheck": "astro check && tsc -b"
}, },
"dependencies": { "dependencies": {
"@astrojs/prefetch": "^0.2.0", "@astrojs/prefetch": "^0.3.0",
"@astrojs/react": "^2.1.0", "@astrojs/react": "^2.2.2",
"@fontsource/jetbrains-mono": "^4.5.12", "@fontsource/jetbrains-mono": "^4.5.12",
"@fontsource/rubik": "^4.5.14", "@fontsource/rubik": "^4.5.14",
"@heroicons/react": "^2.0.16", "@heroicons/react": "^2.0.18",
"@tailwindcss/typography": "^0.5.9", "@reacord/helpers": "workspace:^",
"astro": "^2.1.2", "@tailwindcss/typography": "^0.5.10",
"clsx": "^1.2.1", "astro": "^2.10.9",
"clsx": "^2.0.0",
"reacord": "workspace:*", "reacord": "workspace:*",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"tailwind-merge": "^1.14.0"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/tailwind": "^3.1.0", "@astrojs/tailwind": "^4.0.0",
"@types/node": "*", "@total-typescript/ts-reset": "^0.5.1",
"@types/react": "^18.0.28", "@types/node": "^20.7.0",
"@types/react-dom": "^18.0.11", "@types/react": "^18.2.23",
"@types/react-dom": "^18.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.3.3",
"typedoc": "^0.23.26", "typedoc": "^0.25.1",
"typescript": "^4.9.5",
"wait-on": "^7.0.1" "wait-on": "^7.0.1"
} }
} }

View File

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

View File

@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react"
import blobComfyUrl from "~/assets/blob-comfy.png" import blobComfyUrl from "~/assets/blob-comfy.png"
import cursorIbeamUrl from "~/assets/cursor-ibeam.png" import cursorIbeamUrl from "~/assets/cursor-ibeam.png"
import cursorUrl from "~/assets/cursor.png" import cursorUrl from "~/assets/cursor.png"
import { raise } from "@reacord/helpers/raise.ts"
const defaultState = { const defaultState = {
chatInputText: "", chatInputText: "",
@@ -35,6 +36,7 @@ export function LandingAnimation() {
let running = true let running = true
void (async () => { void (async () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (running) { while (running) {
setState(defaultState) setState(defaultState)
await delay(700) await delay(700)
@@ -69,7 +71,7 @@ export function LandingAnimation() {
count: state.count + 1, count: state.count + 1,
chatInputCursorVisible: false, chatInputCursorVisible: false,
})) }))
animateClick(addRef.current!) animateClick(addRef.current ?? raise("addRef is null"))
await delay(700) await delay(700)
} }
@@ -81,7 +83,7 @@ export function LandingAnimation() {
})) }))
await delay(1000) await delay(1000)
animateClick(deleteRef.current!) animateClick(deleteRef.current ?? raise("deleteRef is null"))
setState((state) => ({ ...state, messageVisible: false })) setState((state) => ({ ...state, messageVisible: false }))
await delay(1000) await delay(1000)
@@ -102,17 +104,21 @@ export function LandingAnimation() {
let running = true let running = true
void (async () => { void (async () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (running) { 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 // check if the cursor is in the input
const cursorRect = cursorRef.current!.getBoundingClientRect() const cursorRect = cursor.getBoundingClientRect()
const chatInputRect = chatInputRef.current!.getBoundingClientRect() const chatInputRect = chatInput.getBoundingClientRect()
const isOverInput = const isOverInput =
cursorRef.current && cursorRef.current &&
chatInputRef.current && chatInputRef.current &&
cursorRect.top + cursorRect.height / 2 > chatInputRect.top cursorRect.top + cursorRect.height / 2 > chatInputRect.top
cursorRef.current!.src = isOverInput ? cursorIbeamUrl : cursorUrl cursor.src = isOverInput ? cursorIbeamUrl : cursorUrl
await animationFrame() await animationFrame()
} }
@@ -125,21 +131,21 @@ export function LandingAnimation() {
return ( return (
<div <div
className="grid gap-2 relative pointer-events-none select-none" className="animate-fade-in pointer-events-none relative grid select-none gap-2"
role="presentation" role="presentation"
> >
<div <div
className={clsx( className={clsx(
"bg-slate-800 p-4 rounded-lg shadow transition", "rounded-lg bg-slate-800 p-4 shadow transition",
state.messageVisible ? "opacity-100" : "opacity-0 -translate-y-2", state.messageVisible ? "opacity-100" : "-translate-y-2 opacity-0",
)} )}
> >
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-12 h-12 p-2 rounded-full bg-no-repeat bg-contain bg-black/25"> <div className="h-12 w-12 rounded-full bg-black/25 bg-contain bg-no-repeat p-2">
<img <img
src={blobComfyUrl} src={blobComfyUrl}
alt="" alt=""
className="object-contain scale-90 w-full h-full" className="h-full w-full scale-90 object-contain"
/> />
</div> </div>
<div> <div>
@@ -148,13 +154,13 @@ export function LandingAnimation() {
<div className="mt-2 flex flex-row gap-3"> <div className="mt-2 flex flex-row gap-3">
<div <div
ref={addRef} ref={addRef}
className="bg-emerald-700 text-white py-1.5 px-3 text-sm rounded" className="rounded bg-emerald-700 px-3 py-1.5 text-sm text-white"
> >
+1 +1
</div> </div>
<div <div
ref={deleteRef} ref={deleteRef}
className="bg-red-700 text-white py-1.5 px-3 text-sm rounded" className="rounded bg-red-700 px-3 py-1.5 text-sm text-white"
> >
🗑 delete 🗑 delete
</div> </div>
@@ -163,12 +169,12 @@ export function LandingAnimation() {
</div> </div>
</div> </div>
<div <div
className="bg-slate-700 pb-2 pt-1.5 px-4 rounded-lg shadow" className="rounded-lg bg-slate-700 px-4 pb-2 pt-1.5 shadow"
ref={chatInputRef} ref={chatInputRef}
> >
<span <span
className={clsx( className={clsx(
"text-sm after:content-[attr(data-after)] after:relative after:-top-px after:-left-[2px]", "text-sm after:relative after:-left-[2px] after:-top-px after:content-[attr(data-after)]",
state.chatInputCursorVisible state.chatInputCursorVisible
? "after:opacity-100" ? "after:opacity-100"
: "after:opacity-0", : "after:opacity-0",
@@ -176,7 +182,7 @@ export function LandingAnimation() {
data-after="|" data-after="|"
> >
{state.chatInputText || ( {state.chatInputText || (
<span className="opacity-50 block absolute translate-y-1"> <span className="absolute block translate-y-1 opacity-50">
Message #showing-off-reacord Message #showing-off-reacord
</span> </span>
)} )}
@@ -186,7 +192,7 @@ export function LandingAnimation() {
<img <img
src={cursorUrl} src={cursorUrl}
alt="" alt=""
className="transition-all duration-500 absolute scale-75 bg-transparent" className="absolute scale-75 bg-transparent transition-all duration-500"
style={{ left: state.cursorLeft, bottom: state.cursorBottom }} style={{ left: state.cursorLeft, bottom: state.cursorBottom }}
ref={cursorRef} ref={cursorRef}
/> />

View File

@@ -4,7 +4,6 @@ import "@fontsource/rubik/variable.css"
import packageJson from "reacord/package.json" import packageJson from "reacord/package.json"
import bannerUrl from "~/assets/banner.png" import bannerUrl from "~/assets/banner.png"
import faviconUrl from "~/assets/favicon.png" import faviconUrl from "~/assets/favicon.png"
import "~/styles/prism-theme.css"
import "~/styles/tailwind.css" import "~/styles/tailwind.css"
--- ---

View File

@@ -29,6 +29,7 @@ const links = [
href: "https://github.com/itsMapleLeaf/reacord", href: "https://github.com/itsMapleLeaf/reacord",
label: "GitHub", label: "GitHub",
icon: ArrowTopRightOnSquareIcon, icon: ArrowTopRightOnSquareIcon,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
component: ExternalLink, component: ExternalLink,
}, },
] ]
@@ -36,17 +37,17 @@ const links = [
const guides = await getCollection("guides") const guides = await getCollection("guides")
--- ---
<nav class="flex justify-between items-center h-16"> <nav class="flex h-16 items-center justify-between">
<a href="/"> <a href="/">
<AppLogo class="w-32" /> <AppLogo class="w-32" />
<span class="sr-only">Home</span> <span class="sr-only">Home</span>
</a> </a>
<div class="hidden md:flex gap-4"> <div class="hidden gap-4 md:flex">
{ {
links.map((link) => ( links.map((link) => (
<link.component <link.component
href={link.href} href={link.href}
class="link inline-flex gap-1 items-center" class="link inline-flex items-center gap-1"
rel={link.prefetch ? "prefetch" : undefined} rel={link.prefetch ? "prefetch" : undefined}
> >
<link.icon className="inline-icon" /> <link.icon className="inline-icon" />

View File

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

View File

@@ -79,8 +79,8 @@ To reply to a command interaction, use the `.reply()` function. This function re
```jsx ```jsx
import { Client } from "discord.js" import { Client } from "discord.js"
import * as React from "react"
import { Button, ReacordDiscordJs } from "reacord" import { Button, ReacordDiscordJs } from "reacord"
import * as React from "react"
const client = new Client({ intents: [] }) const client = new Client({ intents: [] })
const reacord = new ReacordDiscordJs(client) const reacord = new ReacordDiscordJs(client)

View File

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

View File

@@ -1,12 +1,12 @@
--- ---
import { type GetStaticPaths } from "astro" import type { GetStaticPaths } from "astro"
import { getCollection, type CollectionEntry } from "astro:content" import { getCollection, type CollectionEntry } from "astro:content"
import AppFooter from "~/components/app-footer.astro" import AppFooter from "~/components/app-footer.astro"
import Layout from "~/components/layout.astro" import Layout from "~/components/layout.astro"
import MainNavigation from "~/components/main-navigation.astro" import MainNavigation from "~/components/main-navigation.astro"
import NavLink from "~/components/nav-link.astro" import NavLink from "~/components/nav-link.astro"
export type Props = { export interface Props {
guide: CollectionEntry<"guides"> guide: CollectionEntry<"guides">
} }
@@ -25,16 +25,18 @@ const { Content } = await Astro.props.guide.render()
<Layout> <Layout>
<div class="isolate"> <div class="isolate">
<header <header
class="bg-slate-700/30 shadow sticky top-0 backdrop-blur-sm transition z-10 flex" class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
> >
<div class="container"> <div class="container">
<MainNavigation /> <MainNavigation />
</div> </div>
</header> </header>
<main class="container mt-8 flex items-start gap-4"> <main class="container mt-8 flex items-start gap-4">
<nav class="w-48 sticky top-24 hidden md:block"> <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> <h2 class="text-2xl">Guides</h2>
<ul class="mt-3 flex flex-col gap-2 items-start"> <ul class="flex flex-col items-start gap-2">
{ {
guides.map((guide) => ( guides.map((guide) => (
<li> <li>
@@ -49,15 +51,45 @@ const { Content } = await Astro.props.guide.render()
)) ))
} }
</ul> </ul>
<AppFooter class="mt-auto" />
</nav> </nav>
<section <article class="-mt-8 min-w-0 max-w-none flex-1 pb-8">
class="prose prose-invert prose prose-invert prose-h1:font-light prose-h1:mb-4 prose-h1:text-3xl lg:prose-h1:text-4xl prose-h2:font-light prose-h3:font-light prose-p:my-3 prose-a:font-medium prose-a:text-emerald-400 hover:prose-a:no-underline prose-strong:font-medium prose-strong:text-emerald-400 prose-pre:font-monospace prose-pre:overflow-x-auto prose-code:before:hidden prose-code:after:hidden prose-code:text-slate-400 prose-li:mb-5 max-w-none pb-8 flex-1 min-w-0"
>
<Content /> <Content />
</section> </article>
</main> </main>
<div class="py-2"> <AppFooter class="mx-auto mb-4 text-center md:hidden" />
<AppFooter />
</div>
</div> </div>
</Layout> </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

@@ -13,18 +13,18 @@ import MainNavigation from "~/components/main-navigation.astro"
style={{ backgroundImage: `url(${dotsBackgroundUrl})` }} style={{ backgroundImage: `url(${dotsBackgroundUrl})` }}
> >
</div> </div>
<div class="flex flex-col relative min-w-0 min-h-screen pb-4 gap-4"> <div class="relative flex min-h-screen min-w-0 flex-col gap-4 pb-4">
<header class="container"> <header class="container">
<MainNavigation /> <MainNavigation />
</header> </header>
<div class="flex flex-col gap-4 my-auto px-4"> <div class="my-auto flex flex-col gap-4 px-4">
<AppLogo class="w-full max-w-lg mx-auto" /> <AppLogo class="mx-auto w-full max-w-lg" />
<div class="max-w-md w-full mx-auto isolate"> <div class="isolate mx-auto h-44 w-full max-w-md">
<LandingAnimation client:only /> <LandingAnimation client:only />
</div> </div>
<p class="text-center text-lg font-light -mb-1"> <p class="-mb-1 text-center text-lg font-light">
Create interactive Discord messages with React. Create interactive Discord messages with React.
</p> </p>
@@ -47,7 +47,7 @@ import MainNavigation from "~/components/main-navigation.astro"
</div> </div>
</div> </div>
<div class="text-center"> <div class="container text-center">
<AppFooter /> <AppFooter />
</div> </div>
</div> </div>

View File

@@ -1,133 +0,0 @@
/**
* Nord Theme Originally by Arctic Ice Studio
* https://nordtheme.com
*
* Ported for PrismJS by Zane Hitchcoxc (@zwhitchcox) and Gabriel Ramos (@gabrieluizramos)
*/
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
/* font-family: "Fira Code", Consolas, Monaco, "Andale Mono", "Ubuntu Mono",
monospace; */
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.7;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
/* background: #2e3440; */
background: rgba(0, 0, 0, 0.3);
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #636f88;
}
.token.punctuation {
color: #81a1c1;
}
.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #81a1c1;
}
.token.number {
color: #b48ead;
}
.token.boolean {
color: #81a1c1;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #a3be8c;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #81a1c1;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #88c0d0;
}
.token.keyword {
color: #81a1c1;
}
.token.regex,
.token.important {
color: #ebcb8b;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.code-line.highlight-line {
background-color: rgba(255, 255, 255, 0.08);
padding: 0 1rem;
margin: 0 -1rem;
display: block;
}

View File

@@ -7,7 +7,7 @@
@apply outline-none; @apply outline-none;
} }
:focus-visible { :focus-visible {
@apply ring-2 ring-emerald-500 ring-inset; @apply ring-2 ring-inset ring-emerald-500;
} }
pre, pre,
@@ -26,10 +26,10 @@
} }
.link { .link {
@apply font-medium inline-block relative opacity-60 hover:opacity-100 transition-opacity; @apply relative inline-block font-medium opacity-60 transition-opacity hover:opacity-100;
} }
.link::after { .link::after {
@apply content-[''] bottom-[-2px] absolute block w-full h-px bg-current translate-y-[3px] opacity-0 transition; @apply absolute bottom-[-2px] block h-px w-full translate-y-[3px] bg-current opacity-0 transition content-[''];
} }
.link:hover::after { .link:hover::after {
@apply -translate-y-px opacity-50; @apply -translate-y-px opacity-50;
@@ -39,9 +39,21 @@
} }
.button { .button {
@apply inline-block mt-4 px-4 py-2.5 text-xl transition rounded-lg bg-black/25 hover:bg-black/40 hover:-translate-y-0.5 hover:shadow active:translate-y-0 active:transition-none; @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 { .button-solid {
@apply bg-emerald-700 hover:bg-emerald-800; @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,18 +0,0 @@
// @ts-nocheck
module.exports = {
content: ["./src/**/*.{ts,tsx,md,astro}"],
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: [require("@tailwindcss/typography")],
}

View File

@@ -0,0 +1,7 @@
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,5 +1,5 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base",
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "react", "jsxImportSource": "react",
@@ -8,15 +8,5 @@
"~/*": ["./src/*"] "~/*": ["./src/*"]
} }
}, },
"include": ["**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs", "**/*.astro"], "exclude": ["node_modules", "dist", "public/api"]
"exclude": [
"**/node_modules/**",
"**/coverage/**",
"**/build/**",
"**/dist/**",
"**/.cache/**",
"**/api/_build/**",
"**/public/**",
"app"
]
} }

6033
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

16
tailwind.config.ts Normal file
View File

@@ -0,0 +1,16 @@
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,24 +1,3 @@
{ {
"extends": "@itsmapleleaf/configs/tsconfig.base", "extends": "@itsmapleleaf/configs/tsconfig"
"compilerOptions": {
"noImplicitOverride": true
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.cjs",
"**/*.mjs",
"**/*.astro",
".eslintrc.cjs"
],
"exclude": [
"**/node_modules/**",
"**/coverage/**",
"**/build/**",
"**/dist/**",
"**/.cache/**",
"**/api/_build/**",
"**/public/**",
"**/cypress/**"
]
} }

4
tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.base.json",
"exclude": ["node_modules", "packages"]
}