@xenide-io/the-old-ui-theme 0.4.8 → 0.4.9

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenide-io/the-old-ui-theme",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Props-driven React components and theme tokens inspired by classic interface design — optimized for Next.js apps and internal tools.",
5
5
  "author": "Xenide",
6
6
  "license": "MIT",
@@ -131,6 +131,8 @@
131
131
  "access": "public"
132
132
  },
133
133
  "dependencies": {
134
+ "@blocknote/core": "0.52.1",
135
+ "@blocknote/react": "0.52.1",
134
136
  "@radix-ui/react-dialog": "^1.1.23",
135
137
  "@radix-ui/react-dropdown-menu": "^2.1.24",
136
138
  "clsx": "^2.1.1",
@@ -475,6 +475,28 @@ button:hover > .ph-card {
475
475
  -webkit-overflow-scrolling: touch;
476
476
  }
477
477
 
478
+ /* ── Ask AI answer content ───────────────────────────────────────────── */
479
+ .suite-ai-blocknote .bn-editor {
480
+ min-height: 0;
481
+ padding: 0;
482
+ background: transparent;
483
+ color: var(--ph-text-primary);
484
+ font-family: inherit;
485
+ font-size: 0.875rem;
486
+ }
487
+ .suite-ai-blocknote .bn-block-content {
488
+ padding-block: 2px;
489
+ }
490
+ .suite-ai-blocknote [data-content-type="heading"] {
491
+ padding-top: 8px;
492
+ }
493
+ .suite-ai-blocknote [data-content-type="codeBlock"] {
494
+ overflow-x: auto;
495
+ border: 1px solid var(--ph-border);
496
+ border-radius: 6px;
497
+ background: var(--ph-surface);
498
+ }
499
+
478
500
  /* ── Auth / OAuth / launch handoff ──────────────────────────────────────
479
501
  One calm opacity fade — no stacked entrance reveals on bridge pages. */
480
502
  .suite-auth-handoff {
@@ -0,0 +1,40 @@
1
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import SuiteAiBlockNoteMessage from "./ai-message-blocknote";
5
+ import { SuiteThemeProvider } from "./theme-provider";
6
+
7
+ const themeConfig = {
8
+ storageKey: "suite-ai-test-theme",
9
+ lightThemeId: "test-light",
10
+ darkThemeId: "test-dark",
11
+ fallbackTheme: "light" as const,
12
+ };
13
+
14
+ describe("SuiteAiBlockNoteMessage", () => {
15
+ beforeEach(() => {
16
+ vi.stubGlobal("localStorage", {
17
+ getItem: () => "light",
18
+ setItem: vi.fn(),
19
+ });
20
+ });
21
+ afterEach(() => {
22
+ cleanup();
23
+ vi.unstubAllGlobals();
24
+ });
25
+
26
+ it("renders Markdown as restricted read-only BlockNote content", async () => {
27
+ const { container } = render(
28
+ <SuiteThemeProvider config={themeConfig}>
29
+ <SuiteAiBlockNoteMessage markdown={"## Summary\n\n- First item"} />
30
+ </SuiteThemeProvider>,
31
+ );
32
+
33
+ await waitFor(() =>
34
+ expect(screen.getByText("Summary")).toBeInTheDocument(),
35
+ );
36
+ expect(screen.getByText("First item")).toBeInTheDocument();
37
+ expect(container.querySelector('[contenteditable="false"]')).toBeTruthy();
38
+ expect(container.querySelector("img")).toBeNull();
39
+ });
40
+ });
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";
5
+ import { BlockNoteViewRaw, useCreateBlockNote } from "@blocknote/react";
6
+ import "@blocknote/react/style.css";
7
+
8
+ import { useSuiteTheme } from "./theme-provider";
9
+
10
+ const assistantSchema = BlockNoteSchema.create({
11
+ blockSpecs: {
12
+ paragraph: defaultBlockSpecs.paragraph,
13
+ heading: defaultBlockSpecs.heading,
14
+ bulletListItem: defaultBlockSpecs.bulletListItem,
15
+ numberedListItem: defaultBlockSpecs.numberedListItem,
16
+ checkListItem: defaultBlockSpecs.checkListItem,
17
+ codeBlock: defaultBlockSpecs.codeBlock,
18
+ quote: defaultBlockSpecs.quote,
19
+ table: defaultBlockSpecs.table,
20
+ divider: defaultBlockSpecs.divider,
21
+ },
22
+ });
23
+
24
+ export default function SuiteAiBlockNoteMessage({
25
+ markdown,
26
+ }: {
27
+ markdown: string;
28
+ }) {
29
+ const { resolvedTheme } = useSuiteTheme();
30
+ const editor = useCreateBlockNote({ schema: assistantSchema });
31
+
32
+ useEffect(() => {
33
+ try {
34
+ const blocks = editor.tryParseMarkdownToBlocks(markdown);
35
+ editor.replaceBlocks(
36
+ editor.document,
37
+ blocks.length > 0 ? blocks : [{ type: "paragraph", content: markdown }],
38
+ );
39
+ } catch {
40
+ editor.replaceBlocks(editor.document, [
41
+ { type: "paragraph", content: markdown },
42
+ ]);
43
+ }
44
+ }, [editor, markdown]);
45
+
46
+ return (
47
+ <BlockNoteViewRaw
48
+ editor={editor}
49
+ editable={false}
50
+ theme={resolvedTheme}
51
+ className="suite-ai-blocknote"
52
+ formattingToolbar={false}
53
+ sideMenu={false}
54
+ slashMenu={false}
55
+ linkToolbar={false}
56
+ filePanel={false}
57
+ tableHandles={false}
58
+ comments={false}
59
+ />
60
+ );
61
+ }
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
+ lazy,
5
+ Suspense,
4
6
  useCallback,
5
7
  useEffect,
6
8
  useRef,
@@ -12,6 +14,10 @@ import {
12
14
  } from 'react';
13
15
  import { Check, Copy, Sparks, Xmark as X } from 'iconoir-react';
14
16
 
17
+ const SuiteAiBlockNoteMessage = lazy(
18
+ () => import('./ai-message-blocknote'),
19
+ );
20
+
15
21
  export const SUITE_OPEN_ASK_AI_EVENT = 'shellstack:open-ask-ai';
16
22
 
17
23
  export type SuiteAskAiOpenDetail = {
@@ -530,7 +536,19 @@ export function SuiteAiPanel({
530
536
  }
531
537
  data-test={isUser ? 'ask-ai-user' : 'ask-ai-assistant'}
532
538
  >
533
- <p className="whitespace-pre-wrap">{message.content}</p>
539
+ {isUser ? (
540
+ <p className="whitespace-pre-wrap">{message.content}</p>
541
+ ) : (
542
+ <Suspense
543
+ fallback={
544
+ <p className="whitespace-pre-wrap">
545
+ {message.content}
546
+ </p>
547
+ }
548
+ >
549
+ <SuiteAiBlockNoteMessage markdown={message.content} />
550
+ </Suspense>
551
+ )}
534
552
  {!isUser ? (
535
553
  <button
536
554
  type="button"