@arcote.tech/arc-ds 0.7.12 → 0.7.14

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-ds",
3
3
  "type": "module",
4
- "version": "0.7.12",
4
+ "version": "0.7.14",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Design System for Arc framework — CVA-based components with display modes and variant overrides",
@@ -30,7 +30,7 @@
30
30
  "tailwind-merge": "^3.5.0"
31
31
  },
32
32
  "peerDependencies": {
33
- "@arcote.tech/arc": "^0.7.12",
33
+ "@arcote.tech/arc": "^0.7.14",
34
34
  "framer-motion": "^12.0.0",
35
35
  "lucide-react": ">=0.400.0",
36
36
  "radix-ui": "^1.0.0",
@@ -1,5 +1,4 @@
1
- import ReactMarkdown from "react-markdown";
2
- import remarkGfm from "remark-gfm";
1
+ import { Markdown } from "../markdown/markdown";
3
2
  import { Button } from "../button/button";
4
3
  import { Bot, User, MessageSquare, Loader2 } from "lucide-react";
5
4
  import type { ChatMessageData } from "./types";
@@ -57,14 +56,9 @@ export function ChatMessage({
57
56
  {showStreamingPlaceholder ? (
58
57
  <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
59
58
  ) : (
60
- <div
61
- data-streaming={message.isStreaming || undefined}
62
- className="chat-markdown space-y-2 text-left [&_p]:m-0 [&_ul]:my-1 [&_ul]:pl-5 [&_ol]:my-1 [&_ol]:pl-5 [&_li]:my-0.5 [&_a]:text-primary [&_a]:underline [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto [&_strong]:font-semibold [&_em]:italic [&_h1]:text-base [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_h3]:text-sm [&_h3]:font-medium [&_blockquote]:border-l-2 [&_blockquote]:border-primary/30 [&_blockquote]:pl-3 [&_blockquote]:italic data-[streaming]:[&_>*:last-child]:after:content-[''] data-[streaming]:[&_>*:last-child]:after:inline-block data-[streaming]:[&_>*:last-child]:after:w-[0.4em] data-[streaming]:[&_>*:last-child]:after:h-[1em] data-[streaming]:[&_>*:last-child]:after:bg-foreground/60 data-[streaming]:[&_>*:last-child]:after:animate-pulse data-[streaming]:[&_>*:last-child]:after:ml-0.5 data-[streaming]:[&_>*:last-child]:after:align-text-bottom"
63
- >
64
- <ReactMarkdown remarkPlugins={[remarkGfm]}>
65
- {message.content}
66
- </ReactMarkdown>
67
- </div>
59
+ <Markdown isStreaming={message.isStreaming}>
60
+ {message.content}
61
+ </Markdown>
68
62
  )}
69
63
  </div>
70
64
 
@@ -0,0 +1,216 @@
1
+ import ReactMarkdown, { type Components } from "react-markdown";
2
+ import remarkGfm from "remark-gfm";
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface MarkdownProps {
6
+ /** Surowy markdown (GFM) do wyrenderowania. */
7
+ children: string;
8
+ /** Dodatkowe klasy na wrapperze (np. typografia z kontekstu konsumenta). */
9
+ className?: string;
10
+ /**
11
+ * Pulsujący caret na końcu ostatniego bloku — dla treści strumieniowanej.
12
+ * Steruje `data-streaming` na wrapperze.
13
+ */
14
+ isStreaming?: boolean;
15
+ }
16
+
17
+ // WAŻNE: wszystkie className to STATYCZNE literały — Tailwind v4 skanuje źródła
18
+ // DS przez `@source` i generuje tylko klasy obecne dosłownie w kodzie. Żadnej
19
+ // dynamicznej konkatenacji nazw klas. `node` (hast) jest odfiltrowywany z każdego
20
+ // mapowania, żeby nie trafił na DOM. `className` z react-markdown (np.
21
+ // `language-ts` na code, `task-list-item` na li) mergujemy przez `cn` —
22
+ // spread `{...props}` PO `className="…"` nadpisałby nasze klasy.
23
+ const components: Components = {
24
+ p: ({ node, className, ...props }) => (
25
+ <p className={cn("my-2 leading-relaxed", className)} {...props} />
26
+ ),
27
+ ul: ({ node, className, ...props }) => (
28
+ <ul
29
+ className={cn(
30
+ "my-2 ml-5 list-disc space-y-1 marker:text-muted-foreground",
31
+ className,
32
+ )}
33
+ {...props}
34
+ />
35
+ ),
36
+ ol: ({ node, className, ...props }) => (
37
+ <ol
38
+ className={cn(
39
+ "my-2 ml-5 list-decimal space-y-1 marker:text-muted-foreground",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ ),
45
+ li: ({ node, className, ...props }) => {
46
+ // GFM oznacza pozycje task-listy klasą `task-list-item` i wstawia checkbox
47
+ // jako pierwsze dziecko — wtedy bez punktora, w jednej linii z checkboxem.
48
+ const isTask =
49
+ typeof className === "string" && className.includes("task-list-item");
50
+ return (
51
+ <li
52
+ className={cn(
53
+ isTask
54
+ ? "-ml-5 flex list-none items-start gap-2"
55
+ : "pl-1 [&>ol]:mt-1 [&>ol]:mb-0 [&>ul]:mt-1 [&>ul]:mb-0",
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ );
61
+ },
62
+ input: ({ node, className, ...props }) => (
63
+ // GFM renderuje `<input type=checkbox disabled checked?>` — read-only,
64
+ // żeby React nie ostrzegał o kontrolce bez `onChange`.
65
+ <input
66
+ className={cn("mt-1 accent-primary", className)}
67
+ readOnly
68
+ {...props}
69
+ />
70
+ ),
71
+ h1: ({ node, className, ...props }) => (
72
+ <h1 className={cn("mt-4 mb-2 text-lg font-semibold", className)} {...props} />
73
+ ),
74
+ h2: ({ node, className, ...props }) => (
75
+ <h2
76
+ className={cn("mt-4 mb-2 text-base font-semibold", className)}
77
+ {...props}
78
+ />
79
+ ),
80
+ h3: ({ node, className, ...props }) => (
81
+ <h3
82
+ className={cn("mt-3 mb-1.5 text-sm font-semibold", className)}
83
+ {...props}
84
+ />
85
+ ),
86
+ h4: ({ node, className, ...props }) => (
87
+ <h4 className={cn("mt-3 mb-1 text-sm font-medium", className)} {...props} />
88
+ ),
89
+ h5: ({ node, className, ...props }) => (
90
+ <h5
91
+ className={cn(
92
+ "mt-2 mb-1 text-xs font-semibold tracking-wide text-muted-foreground uppercase",
93
+ className,
94
+ )}
95
+ {...props}
96
+ />
97
+ ),
98
+ h6: ({ node, className, ...props }) => (
99
+ <h6
100
+ className={cn(
101
+ "mt-2 mb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase",
102
+ className,
103
+ )}
104
+ {...props}
105
+ />
106
+ ),
107
+ a: ({ node, className, ...props }) => (
108
+ <a
109
+ className={cn(
110
+ "text-primary underline underline-offset-2 break-words hover:opacity-80",
111
+ className,
112
+ )}
113
+ target="_blank"
114
+ rel="noopener noreferrer"
115
+ {...props}
116
+ />
117
+ ),
118
+ code: ({ node, className, ...props }) => (
119
+ // Inline code. Wewnątrz `pre` tło/padding/rozmiar resetuje override `pre`
120
+ // (selektor `pre > code` ma wyższą specyficzność) — brak podwójnego tła.
121
+ <code
122
+ className={cn(
123
+ "rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]",
124
+ className,
125
+ )}
126
+ {...props}
127
+ />
128
+ ),
129
+ pre: ({ node, className, ...props }) => (
130
+ <pre
131
+ className={cn(
132
+ "my-2 overflow-x-auto rounded-md bg-muted p-3 text-xs [&>code]:bg-transparent [&>code]:p-0 [&>code]:font-mono [&>code]:text-inherit",
133
+ className,
134
+ )}
135
+ {...props}
136
+ />
137
+ ),
138
+ blockquote: ({ node, className, ...props }) => (
139
+ <blockquote
140
+ className={cn(
141
+ "my-2 border-l-2 border-primary/30 pl-3 text-muted-foreground italic",
142
+ className,
143
+ )}
144
+ {...props}
145
+ />
146
+ ),
147
+ hr: ({ node, className, ...props }) => (
148
+ <hr className={cn("my-4 border-t border-border", className)} {...props} />
149
+ ),
150
+ table: ({ node, className, ...props }) => (
151
+ <div className="my-2 overflow-x-auto">
152
+ <table
153
+ className={cn("w-full border-collapse text-sm", className)}
154
+ {...props}
155
+ />
156
+ </div>
157
+ ),
158
+ thead: ({ node, className, ...props }) => (
159
+ <thead className={cn("bg-muted", className)} {...props} />
160
+ ),
161
+ th: ({ node, className, ...props }) => (
162
+ <th
163
+ className={cn(
164
+ "border border-border px-3 py-1.5 text-left font-semibold",
165
+ className,
166
+ )}
167
+ {...props}
168
+ />
169
+ ),
170
+ td: ({ node, className, ...props }) => (
171
+ <td className={cn("border border-border px-3 py-1.5", className)} {...props} />
172
+ ),
173
+ del: ({ node, className, ...props }) => (
174
+ <del className={cn("line-through opacity-70", className)} {...props} />
175
+ ),
176
+ img: ({ node, className, ...props }) => (
177
+ <img
178
+ className={cn("my-2 max-w-full rounded-md", className)}
179
+ loading="lazy"
180
+ {...props}
181
+ />
182
+ ),
183
+ strong: ({ node, className, ...props }) => (
184
+ <strong className={cn("font-semibold", className)} {...props} />
185
+ ),
186
+ em: ({ node, className, ...props }) => (
187
+ <em className={cn("italic", className)} {...props} />
188
+ ),
189
+ };
190
+
191
+ /**
192
+ * Renderuje markdown (GFM) spójnie z theme DS (zmienne CSS, dark mode). Pokrywa
193
+ * pełny zakres GFM: listy (z punktorami/numerami + zagnieżdżone), task-listy,
194
+ * tabele (z poziomym scrollem), nagłówki h1–h6, code inline/blok, blockquote,
195
+ * hr, linki (external w nowej karcie), strikethrough, obrazy.
196
+ *
197
+ * `isStreaming` dokleja pulsujący caret na końcu ostatniego bloku — wrapper
198
+ * ustawia `data-streaming`, a caret celuje w `[&>*:last-child]::after`
199
+ * (bezpośrednie ostatnie dziecko, nie głębiej np. w `<li>`).
200
+ */
201
+ export function Markdown({ children, className, isStreaming }: MarkdownProps) {
202
+ return (
203
+ <div
204
+ data-streaming={isStreaming || undefined}
205
+ className={cn(
206
+ "text-left break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
207
+ "data-[streaming]:[&>*:last-child]:after:ml-0.5 data-[streaming]:[&>*:last-child]:after:inline-block data-[streaming]:[&>*:last-child]:after:h-[1em] data-[streaming]:[&>*:last-child]:after:w-[0.4em] data-[streaming]:[&>*:last-child]:after:animate-pulse data-[streaming]:[&>*:last-child]:after:bg-foreground/60 data-[streaming]:[&>*:last-child]:after:align-text-bottom data-[streaming]:[&>*:last-child]:after:content-['']",
208
+ className,
209
+ )}
210
+ >
211
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
212
+ {children}
213
+ </ReactMarkdown>
214
+ </div>
215
+ );
216
+ }
package/src/index.ts CHANGED
@@ -105,6 +105,10 @@ export { ExpandablePanel } from "./layout/expandable-panel";
105
105
  export { useExpandable } from "./layout/use-expandable";
106
106
  export type { UseExpandableReturn } from "./layout/use-expandable";
107
107
 
108
+ // Markdown
109
+ export { Markdown } from "./ds/markdown/markdown";
110
+ export type { MarkdownProps } from "./ds/markdown/markdown";
111
+
108
112
  // Chat
109
113
  export { Chat } from "./ds/chat/chat";
110
114
  export type { ChatProps } from "./ds/chat/chat";