@shadowob/shared 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +28 -7
- package/__tests__/constants.test.ts +0 -69
- package/__tests__/utils.test.ts +0 -80
- package/src/constants/events.ts +0 -28
- package/src/constants/index.ts +0 -2
- package/src/constants/limits.ts +0 -30
- package/src/index.ts +0 -3
- package/src/types/agent.types.ts +0 -44
- package/src/types/channel.types.ts +0 -36
- package/src/types/friendship.types.ts +0 -27
- package/src/types/index.ts +0 -6
- package/src/types/message.types.ts +0 -126
- package/src/types/server.types.ts +0 -38
- package/src/types/user.types.ts +0 -40
- package/src/utils/avatar-generator.ts +0 -231
- package/src/utils/index.ts +0 -24
- package/src/utils/pixel-cats.ts +0 -139
- package/tsconfig.json +0 -8
package/package.json
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowob/shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "./
|
|
6
|
-
"types": "./
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./types": {
|
|
13
|
+
"import": "./dist/types/index.js",
|
|
14
|
+
"types": "./dist/types/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./constants": {
|
|
17
|
+
"import": "./dist/constants/index.js",
|
|
18
|
+
"types": "./dist/constants/index.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./utils": {
|
|
21
|
+
"import": "./dist/utils/index.js",
|
|
22
|
+
"types": "./dist/utils/index.d.ts"
|
|
23
|
+
}
|
|
12
24
|
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
13
28
|
"dependencies": {
|
|
14
29
|
"nanoid": "^5.1.7"
|
|
15
30
|
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsup": "^8.5.0",
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
|
+
},
|
|
16
35
|
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
17
38
|
"test": "vitest run",
|
|
18
39
|
"test:watch": "vitest"
|
|
19
40
|
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { CLIENT_EVENTS, SERVER_EVENTS } from '../src/constants/events'
|
|
3
|
-
import { LIMITS } from '../src/constants/limits'
|
|
4
|
-
|
|
5
|
-
describe('CLIENT_EVENTS', () => {
|
|
6
|
-
it('should have all expected client events', () => {
|
|
7
|
-
expect(CLIENT_EVENTS.CHANNEL_JOIN).toBe('channel:join')
|
|
8
|
-
expect(CLIENT_EVENTS.CHANNEL_LEAVE).toBe('channel:leave')
|
|
9
|
-
expect(CLIENT_EVENTS.MESSAGE_SEND).toBe('message:send')
|
|
10
|
-
expect(CLIENT_EVENTS.MESSAGE_TYPING).toBe('message:typing')
|
|
11
|
-
expect(CLIENT_EVENTS.PRESENCE_UPDATE).toBe('presence:update')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('should be readonly', () => {
|
|
15
|
-
const events = CLIENT_EVENTS
|
|
16
|
-
expect(Object.keys(events)).toHaveLength(5)
|
|
17
|
-
})
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
describe('SERVER_EVENTS', () => {
|
|
21
|
-
it('should have all expected server events', () => {
|
|
22
|
-
expect(SERVER_EVENTS.MESSAGE_NEW).toBe('message:new')
|
|
23
|
-
expect(SERVER_EVENTS.MESSAGE_UPDATE).toBe('message:update')
|
|
24
|
-
expect(SERVER_EVENTS.MESSAGE_DELETE).toBe('message:delete')
|
|
25
|
-
expect(SERVER_EVENTS.MEMBER_TYPING).toBe('member:typing')
|
|
26
|
-
expect(SERVER_EVENTS.MEMBER_JOIN).toBe('member:join')
|
|
27
|
-
expect(SERVER_EVENTS.MEMBER_LEAVE).toBe('member:leave')
|
|
28
|
-
expect(SERVER_EVENTS.PRESENCE_CHANGE).toBe('presence:change')
|
|
29
|
-
expect(SERVER_EVENTS.REACTION_ADD).toBe('reaction:add')
|
|
30
|
-
expect(SERVER_EVENTS.REACTION_REMOVE).toBe('reaction:remove')
|
|
31
|
-
expect(SERVER_EVENTS.NOTIFICATION_NEW).toBe('notification:new')
|
|
32
|
-
expect(SERVER_EVENTS.DM_MESSAGE_NEW).toBe('dm:message:new')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('should have 11 server events', () => {
|
|
36
|
-
expect(Object.keys(SERVER_EVENTS)).toHaveLength(11)
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
describe('LIMITS', () => {
|
|
41
|
-
it('should define message limits', () => {
|
|
42
|
-
expect(LIMITS.MESSAGE_CONTENT_MAX).toBe(4000)
|
|
43
|
-
expect(LIMITS.MESSAGES_PER_PAGE).toBe(50)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('should define username limits', () => {
|
|
47
|
-
expect(LIMITS.USERNAME_MIN).toBe(3)
|
|
48
|
-
expect(LIMITS.USERNAME_MAX).toBe(32)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('should define file upload limit', () => {
|
|
52
|
-
expect(LIMITS.FILE_UPLOAD_MAX_SIZE).toBe(10 * 1024 * 1024)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('should define server/channel limits', () => {
|
|
56
|
-
expect(LIMITS.SERVER_NAME_MAX).toBe(100)
|
|
57
|
-
expect(LIMITS.CHANNEL_NAME_MAX).toBe(100)
|
|
58
|
-
expect(LIMITS.SERVERS_PER_USER_MAX).toBe(100)
|
|
59
|
-
expect(LIMITS.CHANNELS_PER_SERVER_MAX).toBe(200)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('should define invite code length', () => {
|
|
63
|
-
expect(LIMITS.INVITE_CODE_LENGTH).toBe(8)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('should define password min length', () => {
|
|
67
|
-
expect(LIMITS.PASSWORD_MIN).toBe(8)
|
|
68
|
-
})
|
|
69
|
-
})
|
package/__tests__/utils.test.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { formatDate, generateInviteCode, isValidEmail, slugify } from '../src/utils'
|
|
3
|
-
|
|
4
|
-
describe('generateInviteCode', () => {
|
|
5
|
-
it('should generate a string of length 8', () => {
|
|
6
|
-
const code = generateInviteCode()
|
|
7
|
-
expect(code).toHaveLength(8)
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('should only contain alphanumeric characters', () => {
|
|
11
|
-
const code = generateInviteCode()
|
|
12
|
-
expect(code).toMatch(/^[A-Za-z0-9]+$/)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('should generate unique codes', () => {
|
|
16
|
-
const codes = new Set(Array.from({ length: 100 }, () => generateInviteCode()))
|
|
17
|
-
expect(codes.size).toBe(100)
|
|
18
|
-
})
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
describe('formatDate', () => {
|
|
22
|
-
it('should format a Date object to ISO string', () => {
|
|
23
|
-
const date = new Date('2024-01-15T12:30:00Z')
|
|
24
|
-
expect(formatDate(date)).toBe('2024-01-15T12:30:00.000Z')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('should format a date string to ISO string', () => {
|
|
28
|
-
const result = formatDate('2024-01-15T12:30:00Z')
|
|
29
|
-
expect(result).toBe('2024-01-15T12:30:00.000Z')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('should handle various date string formats', () => {
|
|
33
|
-
const result = formatDate('2024-01-15')
|
|
34
|
-
expect(result).toContain('2024-01-15')
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
describe('isValidEmail', () => {
|
|
39
|
-
it('should return true for valid emails', () => {
|
|
40
|
-
expect(isValidEmail('user@shadowob.com')).toBe(true)
|
|
41
|
-
expect(isValidEmail('first.last@domain.org')).toBe(true)
|
|
42
|
-
expect(isValidEmail('user+tag@sub.domain.com')).toBe(true)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('should return false for invalid emails', () => {
|
|
46
|
-
expect(isValidEmail('')).toBe(false)
|
|
47
|
-
expect(isValidEmail('user')).toBe(false)
|
|
48
|
-
expect(isValidEmail('user@')).toBe(false)
|
|
49
|
-
expect(isValidEmail('@domain.com')).toBe(false)
|
|
50
|
-
expect(isValidEmail('user @domain.com')).toBe(false)
|
|
51
|
-
expect(isValidEmail('user@domain')).toBe(false)
|
|
52
|
-
})
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
describe('slugify', () => {
|
|
56
|
-
it('should convert text to lowercase slug', () => {
|
|
57
|
-
expect(slugify('Hello World')).toBe('hello-world')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('should replace special characters with hyphens', () => {
|
|
61
|
-
expect(slugify('Hello, World!')).toBe('hello-world')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('should trim leading/trailing hyphens', () => {
|
|
65
|
-
expect(slugify(' Hello World ')).toBe('hello-world')
|
|
66
|
-
expect(slugify('---hello---')).toBe('hello')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('should handle multiple consecutive special characters', () => {
|
|
70
|
-
expect(slugify('hello...world')).toBe('hello-world')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('should keep numbers', () => {
|
|
74
|
-
expect(slugify('Version 2.0')).toBe('version-2-0')
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('should handle empty string', () => {
|
|
78
|
-
expect(slugify('')).toBe('')
|
|
79
|
-
})
|
|
80
|
-
})
|
package/src/constants/events.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// ─── Client → Server ───
|
|
2
|
-
|
|
3
|
-
export const CLIENT_EVENTS = {
|
|
4
|
-
CHANNEL_JOIN: 'channel:join',
|
|
5
|
-
CHANNEL_LEAVE: 'channel:leave',
|
|
6
|
-
MESSAGE_SEND: 'message:send',
|
|
7
|
-
MESSAGE_TYPING: 'message:typing',
|
|
8
|
-
PRESENCE_UPDATE: 'presence:update',
|
|
9
|
-
} as const
|
|
10
|
-
|
|
11
|
-
// ─── Server → Client ───
|
|
12
|
-
|
|
13
|
-
export const SERVER_EVENTS = {
|
|
14
|
-
MESSAGE_NEW: 'message:new',
|
|
15
|
-
MESSAGE_UPDATE: 'message:update',
|
|
16
|
-
MESSAGE_DELETE: 'message:delete',
|
|
17
|
-
MEMBER_TYPING: 'member:typing',
|
|
18
|
-
MEMBER_JOIN: 'member:join',
|
|
19
|
-
MEMBER_LEAVE: 'member:leave',
|
|
20
|
-
PRESENCE_CHANGE: 'presence:change',
|
|
21
|
-
REACTION_ADD: 'reaction:add',
|
|
22
|
-
REACTION_REMOVE: 'reaction:remove',
|
|
23
|
-
NOTIFICATION_NEW: 'notification:new',
|
|
24
|
-
DM_MESSAGE_NEW: 'dm:message:new',
|
|
25
|
-
} as const
|
|
26
|
-
|
|
27
|
-
export type ClientEvent = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS]
|
|
28
|
-
export type ServerEvent = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS]
|
package/src/constants/index.ts
DELETED
package/src/constants/limits.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export const LIMITS = {
|
|
2
|
-
/** Max message content length */
|
|
3
|
-
MESSAGE_CONTENT_MAX: 4000,
|
|
4
|
-
/** Max username length */
|
|
5
|
-
USERNAME_MAX: 32,
|
|
6
|
-
/** Min username length */
|
|
7
|
-
USERNAME_MIN: 3,
|
|
8
|
-
/** Max display name length */
|
|
9
|
-
DISPLAY_NAME_MAX: 64,
|
|
10
|
-
/** Max server name length */
|
|
11
|
-
SERVER_NAME_MAX: 100,
|
|
12
|
-
/** Max channel name length */
|
|
13
|
-
CHANNEL_NAME_MAX: 100,
|
|
14
|
-
/** Max thread name length */
|
|
15
|
-
THREAD_NAME_MAX: 100,
|
|
16
|
-
/** Max file upload size (10MB) */
|
|
17
|
-
FILE_UPLOAD_MAX_SIZE: 10 * 1024 * 1024,
|
|
18
|
-
/** Messages per page (cursor pagination) */
|
|
19
|
-
MESSAGES_PER_PAGE: 50,
|
|
20
|
-
/** Max servers per user */
|
|
21
|
-
SERVERS_PER_USER_MAX: 100,
|
|
22
|
-
/** Max channels per server */
|
|
23
|
-
CHANNELS_PER_SERVER_MAX: 200,
|
|
24
|
-
/** Invite code length */
|
|
25
|
-
INVITE_CODE_LENGTH: 8,
|
|
26
|
-
/** Password min length */
|
|
27
|
-
PASSWORD_MIN: 8,
|
|
28
|
-
/** Max reactions per message per user */
|
|
29
|
-
REACTIONS_PER_MESSAGE_MAX: 20,
|
|
30
|
-
} as const
|
package/src/index.ts
DELETED
package/src/types/agent.types.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export type AgentStatus = 'running' | 'stopped' | 'error'
|
|
2
|
-
|
|
3
|
-
export type AgentKernelType = 'claude-code' | 'cursor' | 'mcp-server' | 'custom'
|
|
4
|
-
|
|
5
|
-
export type AgentCapability =
|
|
6
|
-
| 'chat'
|
|
7
|
-
| 'code-gen'
|
|
8
|
-
| 'code-review'
|
|
9
|
-
| 'research'
|
|
10
|
-
| 'tools'
|
|
11
|
-
| 'file-access'
|
|
12
|
-
|
|
13
|
-
export interface Agent {
|
|
14
|
-
id: string
|
|
15
|
-
userId: string
|
|
16
|
-
kernelType: AgentKernelType
|
|
17
|
-
config: Record<string, unknown>
|
|
18
|
-
containerId: string | null
|
|
19
|
-
status: AgentStatus
|
|
20
|
-
ownerId: string
|
|
21
|
-
createdAt: string
|
|
22
|
-
updatedAt: string
|
|
23
|
-
user?: {
|
|
24
|
-
id: string
|
|
25
|
-
username: string
|
|
26
|
-
displayName: string
|
|
27
|
-
avatarUrl: string | null
|
|
28
|
-
}
|
|
29
|
-
capabilities?: AgentCapability[]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface CreateAgentRequest {
|
|
33
|
-
name: string
|
|
34
|
-
kernelType: AgentKernelType
|
|
35
|
-
config?: Record<string, unknown>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface AgentInfo {
|
|
39
|
-
id: string
|
|
40
|
-
name: string
|
|
41
|
-
kernelType: AgentKernelType
|
|
42
|
-
status: AgentStatus
|
|
43
|
-
capabilities: AgentCapability[]
|
|
44
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export type ChannelType = 'text' | 'voice' | 'announcement'
|
|
2
|
-
|
|
3
|
-
export interface Channel {
|
|
4
|
-
id: string
|
|
5
|
-
name: string
|
|
6
|
-
type: ChannelType
|
|
7
|
-
serverId: string
|
|
8
|
-
topic: string | null
|
|
9
|
-
position: number
|
|
10
|
-
createdAt: string
|
|
11
|
-
updatedAt: string
|
|
12
|
-
/** Last message timestamp for sorting by activity */
|
|
13
|
-
lastMessageAt?: string | null
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface CreateChannelRequest {
|
|
17
|
-
name: string
|
|
18
|
-
type?: ChannelType
|
|
19
|
-
topic?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface UpdateChannelRequest {
|
|
23
|
-
name?: string
|
|
24
|
-
topic?: string
|
|
25
|
-
position?: number
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Channel sorting options */
|
|
29
|
-
export type ChannelSortBy = 'createdAt' | 'updatedAt' | 'lastMessageAt' | 'lastAccessedAt'
|
|
30
|
-
|
|
31
|
-
export type ChannelSortDirection = 'asc' | 'desc'
|
|
32
|
-
|
|
33
|
-
export interface ChannelSortOptions {
|
|
34
|
-
by: ChannelSortBy
|
|
35
|
-
direction: ChannelSortDirection
|
|
36
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export type FriendshipStatus = 'pending' | 'accepted' | 'blocked'
|
|
2
|
-
|
|
3
|
-
export interface Friendship {
|
|
4
|
-
id: string
|
|
5
|
-
requesterId: string
|
|
6
|
-
addresseeId: string
|
|
7
|
-
status: FriendshipStatus
|
|
8
|
-
createdAt: string
|
|
9
|
-
updatedAt: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type FriendSource = 'friend' | 'owned_claw' | 'rented_claw'
|
|
13
|
-
|
|
14
|
-
export interface FriendEntry {
|
|
15
|
-
friendshipId: string
|
|
16
|
-
/** Where this friend entry comes from */
|
|
17
|
-
source: FriendSource
|
|
18
|
-
user: {
|
|
19
|
-
id: string
|
|
20
|
-
username: string
|
|
21
|
-
displayName: string | null
|
|
22
|
-
avatarUrl: string | null
|
|
23
|
-
status: string
|
|
24
|
-
isBot: boolean
|
|
25
|
-
}
|
|
26
|
-
createdAt: string
|
|
27
|
-
}
|
package/src/types/index.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
export interface Message {
|
|
2
|
-
id: string
|
|
3
|
-
content: string
|
|
4
|
-
channelId: string
|
|
5
|
-
authorId: string
|
|
6
|
-
threadId: string | null
|
|
7
|
-
replyToId: string | null
|
|
8
|
-
isEdited: boolean
|
|
9
|
-
isPinned: boolean
|
|
10
|
-
createdAt: string
|
|
11
|
-
updatedAt: string
|
|
12
|
-
author?: {
|
|
13
|
-
id: string
|
|
14
|
-
username: string
|
|
15
|
-
displayName: string
|
|
16
|
-
avatarUrl: string | null
|
|
17
|
-
isBot: boolean
|
|
18
|
-
}
|
|
19
|
-
attachments?: Attachment[]
|
|
20
|
-
reactions?: ReactionGroup[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface Attachment {
|
|
24
|
-
id: string
|
|
25
|
-
messageId: string
|
|
26
|
-
filename: string
|
|
27
|
-
url: string
|
|
28
|
-
contentType: string
|
|
29
|
-
size: number
|
|
30
|
-
width: number | null
|
|
31
|
-
height: number | null
|
|
32
|
-
createdAt: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ReactionGroup {
|
|
36
|
-
emoji: string
|
|
37
|
-
count: number
|
|
38
|
-
userIds: string[]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface Thread {
|
|
42
|
-
id: string
|
|
43
|
-
name: string
|
|
44
|
-
channelId: string
|
|
45
|
-
parentMessageId: string
|
|
46
|
-
creatorId: string
|
|
47
|
-
isArchived: boolean
|
|
48
|
-
createdAt: string
|
|
49
|
-
updatedAt: string
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface SendMessageRequest {
|
|
53
|
-
content: string
|
|
54
|
-
threadId?: string
|
|
55
|
-
replyToId?: string
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface UpdateMessageRequest {
|
|
59
|
-
content: string
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export type NotificationType = 'mention' | 'reply' | 'dm' | 'system'
|
|
63
|
-
|
|
64
|
-
export interface Notification {
|
|
65
|
-
id: string
|
|
66
|
-
userId: string
|
|
67
|
-
type: NotificationType
|
|
68
|
-
title: string
|
|
69
|
-
body: string | null
|
|
70
|
-
referenceId: string | null
|
|
71
|
-
referenceType: string | null
|
|
72
|
-
isRead: boolean
|
|
73
|
-
createdAt: string
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface DmChannel {
|
|
77
|
-
id: string
|
|
78
|
-
userAId: string
|
|
79
|
-
userBId: string
|
|
80
|
-
lastMessageAt: string | null
|
|
81
|
-
createdAt: string
|
|
82
|
-
otherUser?: {
|
|
83
|
-
id: string
|
|
84
|
-
username: string
|
|
85
|
-
displayName: string
|
|
86
|
-
avatarUrl: string | null
|
|
87
|
-
status: string
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** DM message — mirrors channel Message but scoped to DM channels */
|
|
92
|
-
export interface DmMessage {
|
|
93
|
-
id: string
|
|
94
|
-
content: string
|
|
95
|
-
dmChannelId: string
|
|
96
|
-
authorId: string
|
|
97
|
-
replyToId: string | null
|
|
98
|
-
isEdited: boolean
|
|
99
|
-
createdAt: string
|
|
100
|
-
updatedAt: string
|
|
101
|
-
author?: {
|
|
102
|
-
id: string
|
|
103
|
-
username: string
|
|
104
|
-
displayName: string
|
|
105
|
-
avatarUrl: string | null
|
|
106
|
-
isBot: boolean
|
|
107
|
-
}
|
|
108
|
-
attachments?: DmAttachment[]
|
|
109
|
-
reactions?: ReactionGroup[]
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface DmAttachment {
|
|
113
|
-
id: string
|
|
114
|
-
dmMessageId: string
|
|
115
|
-
filename: string
|
|
116
|
-
url: string
|
|
117
|
-
contentType: string
|
|
118
|
-
size: number
|
|
119
|
-
width: number | null
|
|
120
|
-
height: number | null
|
|
121
|
-
createdAt: string
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface UpdateDmMessageRequest {
|
|
125
|
-
content: string
|
|
126
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export interface Server {
|
|
2
|
-
id: string
|
|
3
|
-
name: string
|
|
4
|
-
iconUrl: string | null
|
|
5
|
-
ownerId: string
|
|
6
|
-
inviteCode: string
|
|
7
|
-
createdAt: string
|
|
8
|
-
updatedAt: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface CreateServerRequest {
|
|
12
|
-
name: string
|
|
13
|
-
iconUrl?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface UpdateServerRequest {
|
|
17
|
-
name?: string
|
|
18
|
-
iconUrl?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type MemberRole = 'owner' | 'admin' | 'member'
|
|
22
|
-
|
|
23
|
-
export interface Member {
|
|
24
|
-
id: string
|
|
25
|
-
userId: string
|
|
26
|
-
serverId: string
|
|
27
|
-
role: MemberRole
|
|
28
|
-
nickname: string | null
|
|
29
|
-
joinedAt: string
|
|
30
|
-
user?: {
|
|
31
|
-
id: string
|
|
32
|
-
username: string
|
|
33
|
-
displayName: string
|
|
34
|
-
avatarUrl: string | null
|
|
35
|
-
status: string
|
|
36
|
-
isBot: boolean
|
|
37
|
-
}
|
|
38
|
-
}
|
package/src/types/user.types.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export type UserStatus = 'online' | 'idle' | 'dnd' | 'offline'
|
|
2
|
-
|
|
3
|
-
export interface User {
|
|
4
|
-
id: string
|
|
5
|
-
email: string
|
|
6
|
-
username: string
|
|
7
|
-
displayName: string
|
|
8
|
-
avatarUrl: string | null
|
|
9
|
-
status: UserStatus
|
|
10
|
-
isBot: boolean
|
|
11
|
-
createdAt: string
|
|
12
|
-
updatedAt: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface UserProfile {
|
|
16
|
-
id: string
|
|
17
|
-
username: string
|
|
18
|
-
displayName: string
|
|
19
|
-
avatarUrl: string | null
|
|
20
|
-
status: UserStatus
|
|
21
|
-
isBot: boolean
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface LoginRequest {
|
|
25
|
-
email: string
|
|
26
|
-
password: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface RegisterRequest {
|
|
30
|
-
email: string
|
|
31
|
-
username: string
|
|
32
|
-
displayName: string
|
|
33
|
-
password: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface AuthResponse {
|
|
37
|
-
user: User
|
|
38
|
-
accessToken: string
|
|
39
|
-
refreshToken: string
|
|
40
|
-
}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
export type CatPattern = 'none' | 'tabby' | 'tuxedo' | 'siamese' | 'calico' | 'bicolor'
|
|
2
|
-
export type CatExpression =
|
|
3
|
-
| 'smile'
|
|
4
|
-
| 'open'
|
|
5
|
-
| 'flat'
|
|
6
|
-
| 'sad'
|
|
7
|
-
| 'surprised'
|
|
8
|
-
| 'kawaii'
|
|
9
|
-
| 'winking'
|
|
10
|
-
| 'smirk'
|
|
11
|
-
export type CatDecoration = 'none' | 'glasses' | 'blush' | 'scar' | 'flower' | 'fish' | 'headband'
|
|
12
|
-
export type BgPattern = 'none' | 'dots' | 'stripes' | 'grid' | 'stars'
|
|
13
|
-
|
|
14
|
-
export interface CatConfig {
|
|
15
|
-
bg: string
|
|
16
|
-
bgPattern: BgPattern
|
|
17
|
-
body: string
|
|
18
|
-
pattern: CatPattern
|
|
19
|
-
patternColor: string
|
|
20
|
-
eyeColor: string
|
|
21
|
-
expression: CatExpression
|
|
22
|
-
decoration: CatDecoration
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const COLORS = {
|
|
26
|
-
bg: [
|
|
27
|
-
'transparent',
|
|
28
|
-
'#1e1f22',
|
|
29
|
-
'#313338',
|
|
30
|
-
'#5865F2',
|
|
31
|
-
'#23a559',
|
|
32
|
-
'#da373c',
|
|
33
|
-
'#f472b6',
|
|
34
|
-
'#3b82f6',
|
|
35
|
-
'#fbbf24',
|
|
36
|
-
'#a855f7',
|
|
37
|
-
'#1abc9c',
|
|
38
|
-
'#f39c12',
|
|
39
|
-
'#e74c3c',
|
|
40
|
-
],
|
|
41
|
-
body: [
|
|
42
|
-
'#2d2d30',
|
|
43
|
-
'#e8842c',
|
|
44
|
-
'#e8e8e8',
|
|
45
|
-
'#7a7a80',
|
|
46
|
-
'#d4a574',
|
|
47
|
-
'#6b8094',
|
|
48
|
-
'#f472b6',
|
|
49
|
-
'#c8d6e5',
|
|
50
|
-
'#3e2723',
|
|
51
|
-
'#bdc3c7',
|
|
52
|
-
'#ffb8b8',
|
|
53
|
-
],
|
|
54
|
-
eyes: [
|
|
55
|
-
'#f8e71c',
|
|
56
|
-
'#00f3ff',
|
|
57
|
-
'#4ade80',
|
|
58
|
-
'#60a5fa',
|
|
59
|
-
'#a855f7',
|
|
60
|
-
'#fbbf24',
|
|
61
|
-
'#f87171',
|
|
62
|
-
'#ffc0cb',
|
|
63
|
-
'#1dd1a1',
|
|
64
|
-
'#e056fd',
|
|
65
|
-
],
|
|
66
|
-
pattern: ['#1a1a1c', '#ffffff', '#5a4a46', '#3d3d40', '#9a9aa0', '#d1ccc0', '#2d3436'],
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function getRandomElement<T>(arr: readonly T[]): T {
|
|
70
|
-
return arr[Math.floor(Math.random() * arr.length)]!
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function generateRandomCatConfig(): CatConfig {
|
|
74
|
-
return {
|
|
75
|
-
bg: getRandomElement(COLORS.bg),
|
|
76
|
-
bgPattern: getRandomElement(['none', 'dots', 'stripes', 'grid', 'stars'] as BgPattern[]),
|
|
77
|
-
body: getRandomElement(COLORS.body),
|
|
78
|
-
pattern: getRandomElement([
|
|
79
|
-
'none',
|
|
80
|
-
'tabby',
|
|
81
|
-
'tuxedo',
|
|
82
|
-
'siamese',
|
|
83
|
-
'calico',
|
|
84
|
-
'bicolor',
|
|
85
|
-
] as CatPattern[]),
|
|
86
|
-
patternColor: getRandomElement(COLORS.pattern),
|
|
87
|
-
eyeColor: getRandomElement(COLORS.eyes),
|
|
88
|
-
expression: getRandomElement([
|
|
89
|
-
'smile',
|
|
90
|
-
'open',
|
|
91
|
-
'flat',
|
|
92
|
-
'sad',
|
|
93
|
-
'surprised',
|
|
94
|
-
'kawaii',
|
|
95
|
-
'winking',
|
|
96
|
-
'smirk',
|
|
97
|
-
] as CatExpression[]),
|
|
98
|
-
decoration: getRandomElement([
|
|
99
|
-
'none',
|
|
100
|
-
'glasses',
|
|
101
|
-
'blush',
|
|
102
|
-
'scar',
|
|
103
|
-
'flower',
|
|
104
|
-
'fish',
|
|
105
|
-
'headband',
|
|
106
|
-
] as CatDecoration[]),
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function renderCatSvg(config: CatConfig): string {
|
|
111
|
-
const { bg, bgPattern, body, pattern, patternColor, eyeColor, expression, decoration } = config
|
|
112
|
-
const stroke = '#1a1a1c'
|
|
113
|
-
|
|
114
|
-
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">`
|
|
115
|
-
|
|
116
|
-
if (bg && bg !== 'transparent') {
|
|
117
|
-
svg += `<rect width="100" height="100" fill="${bg}" rx="20" />`
|
|
118
|
-
|
|
119
|
-
const pColor = `rgba(255,255,255,0.15)`
|
|
120
|
-
const cleanBg = bg.replace('#', '')
|
|
121
|
-
if (bgPattern === 'dots') {
|
|
122
|
-
svg += `<pattern id="p-${cleanBg}-dots" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="2" fill="${pColor}"/></pattern><rect width="100" height="100" fill="url(#p-${cleanBg}-dots)" rx="20" />`
|
|
123
|
-
} else if (bgPattern === 'stripes') {
|
|
124
|
-
svg += `<pattern id="p-${cleanBg}-str" x="0" y="0" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="12" stroke="${pColor}" stroke-width="4"/></pattern><rect width="100" height="100" fill="url(#p-${cleanBg}-str)" rx="20" />`
|
|
125
|
-
} else if (bgPattern === 'grid') {
|
|
126
|
-
svg += `<pattern id="p-${cleanBg}-grid" width="16" height="16" patternUnits="userSpaceOnUse"><path d="M 16 0 L 0 0 0 16" fill="none" stroke="${pColor}" stroke-width="1"/></pattern><rect width="100" height="100" fill="url(#p-${cleanBg}-grid)" rx="20" />`
|
|
127
|
-
} else if (bgPattern === 'stars') {
|
|
128
|
-
svg += `<pattern id="p-${cleanBg}-star" width="20" height="20" patternUnits="userSpaceOnUse"><path d="M10,2 L12,8 L18,8 L13,12 L15,18 L10,14 L5,18 L7,12 L2,8 L8,8 Z" fill="${pColor}" transform="scale(0.5) translate(5,5)"/></pattern><rect width="100" height="100" fill="url(#p-${cleanBg}-star)" rx="20" />`
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
svg += `<ellipse cx="50" cy="85" rx="30" ry="6" fill="rgba(0,0,0,0.2)"/>`
|
|
133
|
-
|
|
134
|
-
// Ears
|
|
135
|
-
svg += `<path d="M22,45 C15,22 28,18 34,22 C38,25 40,38 40,38" fill="${body}" stroke="${stroke}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>`
|
|
136
|
-
svg += `<path d="M78,45 C85,22 72,18 66,22 C62,25 60,38 60,38" fill="${body}" stroke="${stroke}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>`
|
|
137
|
-
svg += `<path d="M26,38 C22,26 29,24 33,26 C35,28 36,34 36,34" fill="#ffb8b8" opacity="0.8"/>`
|
|
138
|
-
svg += `<path d="M74,38 C78,26 71,24 67,26 C65,28 64,34 64,34" fill="#ffb8b8" opacity="0.8"/>`
|
|
139
|
-
|
|
140
|
-
// Face
|
|
141
|
-
svg += `<ellipse cx="50" cy="58" rx="38" ry="30" fill="${body}" stroke="${stroke}" stroke-width="2.5"/>`
|
|
142
|
-
|
|
143
|
-
// Patterns
|
|
144
|
-
if (pattern === 'tabby') {
|
|
145
|
-
svg += `<path d="M50,30 L50,40 M42,32 L46,39 M58,32 L54,39" stroke="${patternColor}" stroke-width="3" stroke-linecap="round" opacity="0.7"/>`
|
|
146
|
-
svg += `<path d="M18,55 L26,57 M20,62 L27,62" stroke="${patternColor}" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>`
|
|
147
|
-
svg += `<path d="M82,55 L74,57 M80,62 L73,62" stroke="${patternColor}" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>`
|
|
148
|
-
} else if (pattern === 'tuxedo') {
|
|
149
|
-
svg += `<path d="M50,56 C30,68 28,88 50,88 C72,88 70,68 50,56" fill="${patternColor}" opacity="0.95"/>`
|
|
150
|
-
svg += `<ellipse cx="50" cy="65" rx="16" ry="12" fill="${patternColor}" opacity="0.95"/>`
|
|
151
|
-
} else if (pattern === 'siamese') {
|
|
152
|
-
svg += `<ellipse cx="50" cy="62" rx="20" ry="16" fill="${patternColor}" opacity="0.6"/>`
|
|
153
|
-
} else if (pattern === 'calico') {
|
|
154
|
-
svg += `<path d="M25,40 Q35,30 45,45 Q35,55 25,40" fill="${patternColor}" opacity="0.8"/>`
|
|
155
|
-
svg += `<path d="M75,45 Q65,60 55,50 Q65,35 75,45" fill="#e8842c" opacity="0.8"/>`
|
|
156
|
-
} else if (pattern === 'bicolor') {
|
|
157
|
-
svg += `<path d="M12,58 Q30,30 50,40 Q60,70 50,88 Q12,88 12,58 Z" fill="${patternColor}" opacity="0.8"/>`
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (decoration === 'blush') {
|
|
161
|
-
svg += `<ellipse cx="28" cy="62" rx="5" ry="3" fill="#ff7675" opacity="0.7"/>`
|
|
162
|
-
svg += `<ellipse cx="72" cy="62" rx="5" ry="3" fill="#ff7675" opacity="0.7"/>`
|
|
163
|
-
}
|
|
164
|
-
if (decoration === 'scar') {
|
|
165
|
-
svg += `<path d="M28,42 L40,52 M30,48 L35,43 M34,51 L39,46 M32,53 L37,48" stroke="#d63031" stroke-width="1.5" stroke-linecap="round"/>`
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Eyes
|
|
169
|
-
const drawEye = (cx: number, cy: number, lookDir: number = 0) => {
|
|
170
|
-
if (expression === 'kawaii') {
|
|
171
|
-
return `<path d="M${cx - 8},${cy} Q${cx},${cy - 8} ${cx + 8},${cy} Q${cx},${cy - 3} ${cx - 8},${cy}" fill="${eyeColor}" stroke="${stroke}" stroke-width="1.5"/><circle cx="${cx + lookDir}" cy="${cy - 2}" r="3" fill="white"/><circle cx="${cx - 3 + lookDir}" cy="${cy}" r="1" fill="white"/>`
|
|
172
|
-
} else if (expression === 'winking' && cx > 50) {
|
|
173
|
-
return `<path d="M${cx - 7},${cy + 2} Q${cx},${cy - 4} ${cx + 7},${cy + 2}" fill="none" stroke="${stroke}" stroke-width="2.5" stroke-linecap="round"/>`
|
|
174
|
-
} else if (expression === 'surprised') {
|
|
175
|
-
return `<circle cx="${cx}" cy="${cy}" r="8" fill="white" stroke="${stroke}" stroke-width="1.5"/><circle cx="${cx}" cy="${cy}" r="3" fill="${eyeColor}"/>`
|
|
176
|
-
} else {
|
|
177
|
-
return `<circle cx="${cx}" cy="${cy}" r="7.5" fill="${eyeColor}" stroke="${stroke}" stroke-width="1.5"/><circle cx="${cx - 2 + lookDir}" cy="${cy - 2}" r="2.5" fill="white"/><circle cx="${cx + 2 + lookDir}" cy="${cy + 1}" r="1" fill="white"/>`
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (expression === 'smirk') {
|
|
182
|
-
svg += `<path d="M26,50 L42,50" stroke="${stroke}" stroke-width="2.5" stroke-linecap="round"/>`
|
|
183
|
-
svg += drawEye(66, 52, 2)
|
|
184
|
-
} else {
|
|
185
|
-
svg += drawEye(34, 52, 0)
|
|
186
|
-
svg += drawEye(66, 52, 0)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (decoration === 'glasses') {
|
|
190
|
-
svg += `<circle cx="34" cy="52" r="11" fill="rgba(255,255,255,0.2)" stroke="#2d3436" stroke-width="2.5"/>`
|
|
191
|
-
svg += `<circle cx="66" cy="52" r="11" fill="rgba(255,255,255,0.2)" stroke="#2d3436" stroke-width="2.5"/>`
|
|
192
|
-
svg += `<path d="M45,50 Q50,48 55,50" fill="none" stroke="#2d3436" stroke-width="2.5" stroke-linecap="round"/>`
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Nose
|
|
196
|
-
svg += `<path d="M47,62 L53,62 L50,65 Z" fill="#ff9ff3" stroke="${stroke}" stroke-width="1" stroke-linejoin="round"/>`
|
|
197
|
-
|
|
198
|
-
// Mouth
|
|
199
|
-
if (expression === 'smile' || expression === 'kawaii' || expression === 'winking') {
|
|
200
|
-
svg += `<path d="M42,67 Q46,72 50,67 M50,67 Q54,72 58,67" fill="none" stroke="${stroke}" stroke-width="2" stroke-linecap="round"/>`
|
|
201
|
-
} else if (expression === 'open') {
|
|
202
|
-
svg += `<path d="M46,67 Q50,75 54,67 Z" fill="#ff7675" stroke="${stroke}" stroke-width="1.5" stroke-linejoin="round"/>`
|
|
203
|
-
} else if (expression === 'flat') {
|
|
204
|
-
svg += `<path d="M47,68 L53,68" stroke="${stroke}" stroke-width="2" stroke-linecap="round"/>`
|
|
205
|
-
} else if (expression === 'sad') {
|
|
206
|
-
svg += `<path d="M43,70 Q50,64 57,70" fill="none" stroke="${stroke}" stroke-width="2" stroke-linecap="round"/>`
|
|
207
|
-
} else if (expression === 'surprised') {
|
|
208
|
-
svg += `<circle cx="50" cy="70" r="3" fill="#1a1a1c"/>`
|
|
209
|
-
} else if (expression === 'smirk') {
|
|
210
|
-
svg += `<path d="M46,67 Q52,69 56,64" fill="none" stroke="${stroke}" stroke-width="2" stroke-linecap="round"/>`
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Whiskers
|
|
214
|
-
svg += `<path d="M28,62 L15,60 M28,65 L14,66" stroke="${stroke}" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>`
|
|
215
|
-
svg += `<path d="M72,62 L85,60 M72,65 L86,66" stroke="${stroke}" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>`
|
|
216
|
-
|
|
217
|
-
// Decorations
|
|
218
|
-
if (decoration === 'headband') {
|
|
219
|
-
svg += `<path d="M16,35 Q50,20 84,35" fill="none" stroke="#e17055" stroke-width="6" stroke-linecap="round"/>`
|
|
220
|
-
svg += `<path d="M50,15 L60,25 L50,28 L40,25 Z" fill="#fab1a0" stroke="#e17055" stroke-width="2"/>`
|
|
221
|
-
} else if (decoration === 'flower') {
|
|
222
|
-
svg += `<path d="M75,30 Q80,20 85,30 Q95,25 85,35 Q90,45 80,40 Q70,45 75,35 Q65,25 75,30 Z" fill="#ffeaa7" stroke="#fdcb6e" stroke-width="1.5"/>`
|
|
223
|
-
svg += `<circle cx="80" cy="33" r="3" fill="#d63031"/>`
|
|
224
|
-
} else if (decoration === 'fish') {
|
|
225
|
-
svg += `<path d="M40,82 Q50,75 60,82 L65,78 L65,86 L60,82 Q50,89 40,82 Z" fill="#81ecec" stroke="${stroke}" stroke-width="1.5"/>`
|
|
226
|
-
svg += `<circle cx="45" cy="81" r="1" fill="${stroke}"/>`
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
svg += `</svg>`
|
|
230
|
-
return `data:image/svg+xml,${encodeURIComponent(svg.replace(/\n\s*/g, ''))}`
|
|
231
|
-
}
|
package/src/utils/index.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { customAlphabet } from 'nanoid'
|
|
2
|
-
|
|
3
|
-
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
4
|
-
|
|
5
|
-
export const generateInviteCode = customAlphabet(alphabet, 8)
|
|
6
|
-
|
|
7
|
-
export function formatDate(date: string | Date): string {
|
|
8
|
-
const d = typeof date === 'string' ? new Date(date) : date
|
|
9
|
-
return d.toISOString()
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function isValidEmail(email: string): boolean {
|
|
13
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function slugify(text: string): string {
|
|
17
|
-
return text
|
|
18
|
-
.toLowerCase()
|
|
19
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
20
|
-
.replace(/^-+|-+$/g, '')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export * from './avatar-generator'
|
|
24
|
-
export * from './pixel-cats'
|
package/src/utils/pixel-cats.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pixel Art Cat Avatar System
|
|
3
|
-
* 8 unique cat variants with distinct color schemes inspired by the Shadow logo
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
interface CatColors {
|
|
7
|
-
body: string
|
|
8
|
-
stroke: string
|
|
9
|
-
earInner: string
|
|
10
|
-
eyeL: string
|
|
11
|
-
eyeR: string
|
|
12
|
-
nose: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const variants: CatColors[] = [
|
|
16
|
-
// 0: Shadow — Black cat (logo style)
|
|
17
|
-
{
|
|
18
|
-
body: '#2d2d30',
|
|
19
|
-
stroke: '#1a1a1c',
|
|
20
|
-
earInner: '#3d3d40',
|
|
21
|
-
eyeL: '#f8e71c',
|
|
22
|
-
eyeR: '#00f3ff',
|
|
23
|
-
nose: '#3a2a26',
|
|
24
|
-
},
|
|
25
|
-
// 1: Mikan — Orange tabby
|
|
26
|
-
{
|
|
27
|
-
body: '#e8842c',
|
|
28
|
-
stroke: '#1a1a1c',
|
|
29
|
-
earInner: '#f5a623',
|
|
30
|
-
eyeL: '#4ade80',
|
|
31
|
-
eyeR: '#4ade80',
|
|
32
|
-
nose: '#d46b1a',
|
|
33
|
-
},
|
|
34
|
-
// 2: Yuki — White cat
|
|
35
|
-
{
|
|
36
|
-
body: '#e8e8e8',
|
|
37
|
-
stroke: '#a0a0a0',
|
|
38
|
-
earInner: '#ffc0cb',
|
|
39
|
-
eyeL: '#60a5fa',
|
|
40
|
-
eyeR: '#60a5fa',
|
|
41
|
-
nose: '#f5a0b0',
|
|
42
|
-
},
|
|
43
|
-
// 3: Haiiro — Gray cat
|
|
44
|
-
{
|
|
45
|
-
body: '#7a7a80',
|
|
46
|
-
stroke: '#4a4a50',
|
|
47
|
-
earInner: '#9a9aa0',
|
|
48
|
-
eyeL: '#fbbf24',
|
|
49
|
-
eyeR: '#fbbf24',
|
|
50
|
-
nose: '#5a4a46',
|
|
51
|
-
},
|
|
52
|
-
// 4: Tuxedo — Black & white accents
|
|
53
|
-
{
|
|
54
|
-
body: '#2d2d30',
|
|
55
|
-
stroke: '#1a1a1c',
|
|
56
|
-
earInner: '#e0e0e0',
|
|
57
|
-
eyeL: '#22c55e',
|
|
58
|
-
eyeR: '#22c55e',
|
|
59
|
-
nose: '#3a2a26',
|
|
60
|
-
},
|
|
61
|
-
// 5: Mocha — Cream/beige
|
|
62
|
-
{
|
|
63
|
-
body: '#d4a574',
|
|
64
|
-
stroke: '#8b6914',
|
|
65
|
-
earInner: '#e8c9a0',
|
|
66
|
-
eyeL: '#d97706',
|
|
67
|
-
eyeR: '#d97706',
|
|
68
|
-
nose: '#a0705a',
|
|
69
|
-
},
|
|
70
|
-
// 6: Blue — Russian blue
|
|
71
|
-
{
|
|
72
|
-
body: '#6b8094',
|
|
73
|
-
stroke: '#3d5060',
|
|
74
|
-
earInner: '#8ba0b4',
|
|
75
|
-
eyeL: '#22c55e',
|
|
76
|
-
eyeR: '#22c55e',
|
|
77
|
-
nose: '#5a6a76',
|
|
78
|
-
},
|
|
79
|
-
// 7: Sakura — Fantasy pink
|
|
80
|
-
{
|
|
81
|
-
body: '#f472b6',
|
|
82
|
-
stroke: '#be185d',
|
|
83
|
-
earInner: '#f9a8d4',
|
|
84
|
-
eyeL: '#a855f7',
|
|
85
|
-
eyeR: '#c084fc',
|
|
86
|
-
nose: '#d44a8c',
|
|
87
|
-
},
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
function makeCatSvg(c: CatColors): string {
|
|
91
|
-
return [
|
|
92
|
-
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">',
|
|
93
|
-
`<path d="M22,45 Q15,22 28,22 Q34,22 40,38" fill="${c.body}" stroke="${c.stroke}" stroke-width="2.5" stroke-linecap="round"/>`,
|
|
94
|
-
`<path d="M78,45 Q85,22 72,22 Q66,22 60,38" fill="${c.body}" stroke="${c.stroke}" stroke-width="2.5" stroke-linecap="round"/>`,
|
|
95
|
-
`<path d="M26,38 Q22,26 29,26 Q33,26 36,34" fill="${c.earInner}" opacity="0.5"/>`,
|
|
96
|
-
`<path d="M74,38 Q78,26 71,26 Q67,26 64,34" fill="${c.earInner}" opacity="0.5"/>`,
|
|
97
|
-
`<ellipse cx="50" cy="58" rx="36" ry="28" fill="${c.body}" stroke="${c.stroke}" stroke-width="2.5"/>`,
|
|
98
|
-
`<circle cx="35" cy="52" r="7" fill="${c.eyeL}" stroke="${c.stroke}" stroke-width="1.5"/>`,
|
|
99
|
-
`<circle cx="33" cy="49" r="2.5" fill="white"/>`,
|
|
100
|
-
`<circle cx="65" cy="52" r="7" fill="${c.eyeR}" stroke="${c.stroke}" stroke-width="1.5"/>`,
|
|
101
|
-
`<circle cx="63" cy="49" r="2.5" fill="white"/>`,
|
|
102
|
-
`<ellipse cx="50" cy="62" rx="3.5" ry="2.2" fill="${c.nose}"/>`,
|
|
103
|
-
`<path d="M42,67 Q46,72 50,67" fill="none" stroke="${c.stroke}" stroke-width="2" stroke-linecap="round"/>`,
|
|
104
|
-
`<path d="M50,67 Q54,72 58,67" fill="none" stroke="${c.stroke}" stroke-width="2" stroke-linecap="round"/>`,
|
|
105
|
-
'</svg>',
|
|
106
|
-
].join('')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const catDataUris = variants.map((v) => {
|
|
110
|
-
const svg = makeCatSvg(v)
|
|
111
|
-
return `data:image/svg+xml,${encodeURIComponent(svg)}`
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
/** Get avatar data URI by index (0-7) */
|
|
115
|
-
export function getCatAvatar(index: number): string {
|
|
116
|
-
return catDataUris[index % catDataUris.length]!
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Get deterministic avatar by user ID string */
|
|
120
|
-
export function getCatAvatarByUserId(userId: string): string {
|
|
121
|
-
let hash = 0
|
|
122
|
-
for (let i = 0; i < userId.length; i++) {
|
|
123
|
-
hash = ((hash << 5) - hash + userId.charCodeAt(i)) | 0
|
|
124
|
-
}
|
|
125
|
-
return getCatAvatar(Math.abs(hash) % catDataUris.length)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Get all cat avatars for selection UI */
|
|
129
|
-
export function getAllCatAvatars(): { index: number; name: string; dataUri: string }[] {
|
|
130
|
-
const names = ['影子', '蜜柑', '小雪', '灰灰', '燕尾服', '摩卡', '蓝蓝', '小樱']
|
|
131
|
-
return catDataUris.map((uri, i) => ({ index: i, name: names[i]!, dataUri: uri }))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Get the raw SVG string for a cat variant (useful for generating PNG files) */
|
|
135
|
-
export function getCatSvgString(index: number): string {
|
|
136
|
-
return makeCatSvg(variants[index % variants.length]!)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export const CAT_AVATAR_COUNT = variants.length
|