nucleus-core-ts 0.9.165 → 0.9.167

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.
@@ -1,3 +1,3 @@
1
- import type { ReactElement } from 'react';
1
+ import { type ReactElement } from 'react';
2
2
  import type { ChatPanelProps } from '../types';
3
3
  export declare function ChatPanel(props: ChatPanelProps): ReactElement;
@@ -1,16 +1,27 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
3
4
  import { cn } from '../../../utils/cn';
4
5
  import { useChat } from '../hooks/useChat';
5
6
  import { chatPanelTheme } from '../theme';
6
7
  import { ConversationList } from './ConversationList';
7
8
  import { MessageComposer } from './MessageComposer';
8
9
  import { MessageThread } from './MessageThread';
10
+ import { NewConversationModal } from './NewConversationModal';
9
11
  export function ChatPanel(props) {
10
12
  const theme = chatPanelTheme;
11
13
  const chat = useChat(props);
12
14
  const { store } = chat;
15
+ const [pickerOpen, setPickerOpen] = useState(false);
13
16
  const active = store.conversations.find((c)=>c.id === store.activeConversationId) ?? null;
17
+ const handleNewConversation = ()=>{
18
+ if (props.directory) setPickerOpen(true);
19
+ else props.onNewConversation?.();
20
+ };
21
+ const handleStartDirect = (userId)=>{
22
+ chat.startDirect(userId);
23
+ setPickerOpen(false);
24
+ };
14
25
  const handleSendFiles = (files, text)=>{
15
26
  const conversationId = store.activeConversationId;
16
27
  if (!conversationId) return;
@@ -51,10 +62,10 @@ export function ChatPanel(props) {
51
62
  className: theme.sidebar.title,
52
63
  children: props.title ?? 'Messages'
53
64
  }),
54
- props.onNewConversation && /*#__PURE__*/ _jsx("button", {
65
+ (props.directory || props.onNewConversation) && /*#__PURE__*/ _jsx("button", {
55
66
  type: "button",
56
67
  className: theme.sidebar.newButton,
57
- onClick: props.onNewConversation,
68
+ onClick: handleNewConversation,
58
69
  "aria-label": "New conversation",
59
70
  children: /*#__PURE__*/ _jsxs("svg", {
60
71
  className: "h-4 w-4",
@@ -106,6 +117,15 @@ export function ChatPanel(props) {
106
117
  className: theme.error,
107
118
  children: store.error
108
119
  }),
120
+ props.directory && /*#__PURE__*/ _jsx(NewConversationModal, {
121
+ open: pickerOpen,
122
+ directory: props.directory,
123
+ currentUserId: props.currentUserId,
124
+ theme: theme,
125
+ isCreating: props.actions.createConversation.state.isPending,
126
+ onClose: ()=>setPickerOpen(false),
127
+ onStartDirect: handleStartDirect
128
+ }),
109
129
  active && /*#__PURE__*/ _jsx(MessageComposer, {
110
130
  theme: theme,
111
131
  value: store.composerText,
@@ -0,0 +1,17 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { ChatPanelTheme } from '../theme';
3
+ import type { ChatDirectoryUser } from '../types';
4
+ type NewConversationModalProps = {
5
+ open: boolean;
6
+ directory: ChatDirectoryUser[];
7
+ currentUserId: string;
8
+ theme: ChatPanelTheme;
9
+ isCreating: boolean;
10
+ title?: string;
11
+ searchPlaceholder?: string;
12
+ emptyLabel?: string;
13
+ onClose: () => void;
14
+ onStartDirect: (userId: string) => void;
15
+ };
16
+ export declare function NewConversationModal({ open, directory, currentUserId, theme, isCreating, title, searchPlaceholder, emptyLabel, onClose, onStartDirect, }: NewConversationModalProps): ReactElement | null;
17
+ export {};
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { cn } from '../../../utils/cn';
5
+ import { initialsOf } from '../helpers';
6
+ export function NewConversationModal({ open, directory, currentUserId, theme, isCreating, title = 'New message', searchPlaceholder = 'Search people...', emptyLabel = 'No people found', onClose, onStartDirect }) {
7
+ const [query, setQuery] = useState('');
8
+ if (!open) return null;
9
+ const normalized = query.trim().toLowerCase();
10
+ const people = directory.filter((person)=>person.userId !== currentUserId).filter((person)=>normalized ? person.name.toLowerCase().includes(normalized) || (person.subtitle?.toLowerCase().includes(normalized) ?? false) : true);
11
+ return /*#__PURE__*/ _jsxs("div", {
12
+ className: theme.picker.overlay,
13
+ children: [
14
+ /*#__PURE__*/ _jsx("button", {
15
+ type: "button",
16
+ "aria-label": "Close",
17
+ className: "absolute inset-0 h-full w-full cursor-default",
18
+ onClick: onClose
19
+ }),
20
+ /*#__PURE__*/ _jsxs("div", {
21
+ className: cn('relative', theme.picker.panel),
22
+ role: "dialog",
23
+ "aria-modal": "true",
24
+ "aria-label": title,
25
+ children: [
26
+ /*#__PURE__*/ _jsxs("div", {
27
+ className: theme.picker.header,
28
+ children: [
29
+ /*#__PURE__*/ _jsx("span", {
30
+ className: theme.picker.title,
31
+ children: title
32
+ }),
33
+ /*#__PURE__*/ _jsx("button", {
34
+ type: "button",
35
+ className: theme.picker.closeButton,
36
+ onClick: onClose,
37
+ "aria-label": "Close",
38
+ children: /*#__PURE__*/ _jsxs("svg", {
39
+ className: "h-4 w-4",
40
+ fill: "none",
41
+ viewBox: "0 0 24 24",
42
+ stroke: "currentColor",
43
+ children: [
44
+ /*#__PURE__*/ _jsx("title", {
45
+ children: "Close"
46
+ }),
47
+ /*#__PURE__*/ _jsx("path", {
48
+ strokeLinecap: "round",
49
+ strokeLinejoin: "round",
50
+ strokeWidth: 2,
51
+ d: "M6 18L18 6M6 6l12 12"
52
+ })
53
+ ]
54
+ })
55
+ })
56
+ ]
57
+ }),
58
+ /*#__PURE__*/ _jsx("div", {
59
+ className: theme.picker.searchWrap,
60
+ children: /*#__PURE__*/ _jsx("input", {
61
+ type: "text",
62
+ className: theme.picker.search,
63
+ placeholder: searchPlaceholder,
64
+ value: query,
65
+ onChange: (event)=>setQuery(event.target.value)
66
+ })
67
+ }),
68
+ /*#__PURE__*/ _jsx("div", {
69
+ className: theme.picker.list,
70
+ children: people.length === 0 ? /*#__PURE__*/ _jsx("div", {
71
+ className: theme.picker.empty,
72
+ children: emptyLabel
73
+ }) : people.map((person)=>/*#__PURE__*/ _jsxs("button", {
74
+ type: "button",
75
+ className: theme.picker.item,
76
+ disabled: isCreating,
77
+ onClick: ()=>onStartDirect(person.userId),
78
+ children: [
79
+ /*#__PURE__*/ _jsx("span", {
80
+ className: theme.picker.avatar,
81
+ children: person.avatarUrl ? /*#__PURE__*/ _jsx("img", {
82
+ src: person.avatarUrl,
83
+ alt: person.name,
84
+ className: "h-full w-full rounded-full object-cover"
85
+ }) : initialsOf(person.name)
86
+ }),
87
+ /*#__PURE__*/ _jsxs("span", {
88
+ className: "min-w-0 flex-1",
89
+ children: [
90
+ /*#__PURE__*/ _jsx("span", {
91
+ className: cn('block', theme.picker.name),
92
+ children: person.name
93
+ }),
94
+ person.subtitle && /*#__PURE__*/ _jsx("span", {
95
+ className: cn('block', theme.picker.subtitle),
96
+ children: person.subtitle
97
+ })
98
+ ]
99
+ })
100
+ ]
101
+ }, person.userId))
102
+ })
103
+ ]
104
+ })
105
+ ]
106
+ });
107
+ }
@@ -2,7 +2,8 @@ export { ChatPanel } from './components/ChatPanel';
2
2
  export { ConversationList } from './components/ConversationList';
3
3
  export { MessageComposer } from './components/MessageComposer';
4
4
  export { MessageThread } from './components/MessageThread';
5
+ export { NewConversationModal } from './components/NewConversationModal';
5
6
  export { type UseChatReturn, useChat } from './hooks/useChat';
6
7
  export { useChatStore } from './store';
7
8
  export { type ChatPanelTheme, chatPanelTheme, extendChatPanelTheme } from './theme';
8
- export type { ChatAttachmentDTO, ChatConversationDTO, ChatMessageDTO, ChatPanelActions, ChatPanelProps, ChatPanelState, ChatParticipantDTO, TypingIndicator, } from './types';
9
+ export type { ChatAttachmentDTO, ChatConversationDTO, ChatDirectoryUser, ChatMessageDTO, ChatPanelActions, ChatPanelProps, ChatPanelState, ChatParticipantDTO, TypingIndicator, } from './types';
@@ -2,6 +2,7 @@ export { ChatPanel } from './components/ChatPanel';
2
2
  export { ConversationList } from './components/ConversationList';
3
3
  export { MessageComposer } from './components/MessageComposer';
4
4
  export { MessageThread } from './components/MessageThread';
5
+ export { NewConversationModal } from './components/NewConversationModal';
5
6
  export { useChat } from './hooks/useChat';
6
7
  export { useChatStore } from './store';
7
8
  export { chatPanelTheme, extendChatPanelTheme } from './theme';
@@ -57,6 +57,21 @@ export declare const chatPanelTheme: {
57
57
  readonly pendingChip: "inline-flex items-center gap-1 rounded-lg bg-zinc-100 px-2 py-1 text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300";
58
58
  };
59
59
  readonly error: "border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300";
60
+ readonly picker: {
61
+ readonly overlay: "fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 backdrop-blur-sm sm:items-center";
62
+ readonly panel: "flex max-h-[80vh] w-full max-w-md flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950";
63
+ readonly header: "flex items-center justify-between gap-2 border-b border-zinc-200 px-4 py-3 dark:border-zinc-800";
64
+ readonly title: "text-sm font-semibold";
65
+ readonly closeButton: "inline-flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition hover:bg-zinc-100 dark:hover:bg-zinc-800";
66
+ readonly searchWrap: "border-b border-zinc-200 p-3 dark:border-zinc-800";
67
+ readonly search: "w-full rounded-xl border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm outline-none focus:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-900";
68
+ readonly list: "flex-1 overflow-y-auto p-1.5";
69
+ readonly item: "flex w-full items-center gap-3 rounded-xl px-2.5 py-2 text-left transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-zinc-900";
70
+ readonly avatar: "flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-zinc-200 text-sm font-semibold text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300";
71
+ readonly name: "truncate text-sm font-medium";
72
+ readonly subtitle: "truncate text-xs text-zinc-500 dark:text-zinc-400";
73
+ readonly empty: "px-4 py-8 text-center text-sm text-zinc-400";
74
+ };
60
75
  };
61
76
  export declare function extendChatPanelTheme(overrides: Partial<{
62
77
  [K in keyof ChatPanelTheme]: Partial<ChatPanelTheme[K]>;
@@ -55,7 +55,22 @@ export const chatPanelTheme = {
55
55
  pendingFiles: 'flex flex-wrap gap-1.5 px-4 pb-2',
56
56
  pendingChip: 'inline-flex items-center gap-1 rounded-lg bg-zinc-100 px-2 py-1 text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300'
57
57
  },
58
- error: 'border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300'
58
+ error: 'border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300',
59
+ picker: {
60
+ overlay: 'fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 backdrop-blur-sm sm:items-center',
61
+ panel: 'flex max-h-[80vh] w-full max-w-md flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950',
62
+ header: 'flex items-center justify-between gap-2 border-b border-zinc-200 px-4 py-3 dark:border-zinc-800',
63
+ title: 'text-sm font-semibold',
64
+ closeButton: 'inline-flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition hover:bg-zinc-100 dark:hover:bg-zinc-800',
65
+ searchWrap: 'border-b border-zinc-200 p-3 dark:border-zinc-800',
66
+ search: 'w-full rounded-xl border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm outline-none focus:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-900',
67
+ list: 'flex-1 overflow-y-auto p-1.5',
68
+ item: 'flex w-full items-center gap-3 rounded-xl px-2.5 py-2 text-left transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-zinc-900',
69
+ avatar: 'flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-zinc-200 text-sm font-semibold text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300',
70
+ name: 'truncate text-sm font-medium',
71
+ subtitle: 'truncate text-xs text-zinc-500 dark:text-zinc-400',
72
+ empty: 'px-4 py-8 text-center text-sm text-zinc-400'
73
+ }
59
74
  };
60
75
  export function extendChatPanelTheme(overrides) {
61
76
  const merged = {};
@@ -43,11 +43,25 @@ export type ChatPanelActions = {
43
43
  id: string;
44
44
  }, ChatActionResponse>;
45
45
  };
46
+ /** A selectable person for the built-in "new conversation" people picker. */
47
+ export type ChatDirectoryUser = {
48
+ userId: string;
49
+ name: string;
50
+ avatarUrl?: string;
51
+ /** Secondary line, e.g. job title or department. */
52
+ subtitle?: string;
53
+ };
46
54
  export type ChatPanelProps = {
47
55
  /** The authenticated user's id (must match the verified session). */
48
56
  currentUserId: string;
49
57
  /** Generated API actions wired by the consumer. */
50
58
  actions: ChatPanelActions;
59
+ /**
60
+ * People the current user can start a conversation with. When provided, the
61
+ * "new conversation" button opens a built-in searchable people picker
62
+ * (Facebook/LinkedIn style). The current user is filtered out automatically.
63
+ */
64
+ directory?: ChatDirectoryUser[];
51
65
  /** Absolute WebSocket origin, e.g. "wss://api.example.com". Defaults to current host. */
52
66
  wsUrl?: string;
53
67
  /** WebSocket path (default: "/api/events/subscribe"). */
@@ -9,8 +9,8 @@ export { Captcha, captchaTheme } from './components/Captcha';
9
9
  export type { ChangePasswordAction, ChangePasswordFormProps, ChangePasswordHeaderProps, ChangePasswordPageConfig, ChangePasswordStep, } from './components/ChangePasswordPage';
10
10
  export { ChangePasswordPage, useChangePasswordStore, } from './components/ChangePasswordPage';
11
11
  export { Checkbox } from './components/Checkbox';
12
- export { ChatPanel, chatPanelTheme, extendChatPanelTheme, useChat, useChatStore } from './components/ChatPanel';
13
- export type { ChatPanelActions, ChatPanelProps, ChatPanelState, ChatPanelTheme, TypingIndicator as ChatTypingIndicator, UseChatReturn, } from './components/ChatPanel';
12
+ export { ChatPanel, chatPanelTheme, extendChatPanelTheme, NewConversationModal, useChat, useChatStore, } from './components/ChatPanel';
13
+ export type { ChatDirectoryUser, ChatPanelActions, ChatPanelProps, ChatPanelState, ChatPanelTheme, TypingIndicator as ChatTypingIndicator, UseChatReturn, } from './components/ChatPanel';
14
14
  export { DataTable } from './components/DataTable';
15
15
  export { DatePicker } from './components/DatePicker';
16
16
  export type { ComponentCategory, ComponentEntry, ComponentExample, DesignSystemMethods, DesignSystemPageProps, DesignSystemState, PropControl, PropControlOption, PropControlType, ThemeMode, ViewportSize, } from './components/DesignSystem';
package/dist/fe/index.js CHANGED
@@ -8,7 +8,7 @@ export { Captcha, captchaTheme } from './components/Captcha';
8
8
  export { ChangePasswordPage, useChangePasswordStore } from './components/ChangePasswordPage';
9
9
  export { Checkbox } from './components/Checkbox';
10
10
  // Chat
11
- export { ChatPanel, chatPanelTheme, extendChatPanelTheme, useChat, useChatStore } from './components/ChatPanel';
11
+ export { ChatPanel, chatPanelTheme, extendChatPanelTheme, NewConversationModal, useChat, useChatStore } from './components/ChatPanel';
12
12
  export { DataTable } from './components/DataTable';
13
13
  export { DatePicker } from './components/DatePicker';
14
14
  export { ComponentCanvas, ComponentSidebar, createDefaultRegistry, DesignSystemPage, designSystemTheme, extendDesignSystemTheme, PropsPanel, useDesignSystemStore } from './components/DesignSystem';
@@ -2322,17 +2322,25 @@ export type AllGeneratedEndpoints<TEntities extends readonly {
2322
2322
  }[], TExtra extends Record<string, EndpointDefinition> = Record<string, never>> = GeneratedEndpointsFromConfig<TEntities> & AuthEndpointDefinitions & MonitoringEndpointDefinitions & TExtra;
2323
2323
  type NucleusColumnTypeToTS<T extends string> = T extends 'integer' | 'smallint' | 'bigint' | 'serial' | 'smallserial' | 'bigserial' | 'real' | 'doublePrecision' | 'numeric' | 'decimal' ? number : T extends 'text' | 'varchar' | 'char' | 'uuid' ? string : T extends 'boolean' ? boolean : T extends 'date' | 'time' | 'timestamp' | 'timestamptz' ? Date | string : T extends 'json' | 'jsonb' ? unknown : unknown;
2324
2324
  type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${T}${Capitalize<SnakeToCamelCase<U>>}` : S;
2325
+ /** Maps a column to its TS type, wrapping `array: true` columns as `T[]`. */
2326
+ type ColumnTsType<C extends {
2327
+ type: string;
2328
+ array?: boolean;
2329
+ }> = C extends {
2330
+ array: true;
2331
+ } ? NucleusColumnTypeToTS<C['type']>[] : NucleusColumnTypeToTS<C['type']>;
2325
2332
  type ColumnsToEntity<TColumns extends readonly {
2326
2333
  name: string;
2327
2334
  type: string;
2335
+ array?: boolean;
2328
2336
  }[]> = {
2329
2337
  [C in Extract<TColumns[number], {
2330
2338
  notNull: true;
2331
- }> as SnakeToCamelCase<C['name']>]: NucleusColumnTypeToTS<C['type']>;
2339
+ }> as SnakeToCamelCase<C['name']>]: ColumnTsType<C>;
2332
2340
  } & {
2333
2341
  [C in Exclude<TColumns[number], {
2334
2342
  notNull: true;
2335
- }> as SnakeToCamelCase<C['name']>]: NucleusColumnTypeToTS<C['type']> | null;
2343
+ }> as SnakeToCamelCase<C['name']>]: ColumnTsType<C> | null;
2336
2344
  };
2337
2345
  type BaseEntityFields = {
2338
2346
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.165",
3
+ "version": "0.9.167",
4
4
  "description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
5
5
  "author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",