@specglass/theme-default 0.0.2

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.
Files changed (141) hide show
  1. package/dist/__tests__/code-tabs.test.d.ts +2 -0
  2. package/dist/__tests__/code-tabs.test.d.ts.map +1 -0
  3. package/dist/__tests__/code-tabs.test.js +219 -0
  4. package/dist/__tests__/code-tabs.test.js.map +1 -0
  5. package/dist/__tests__/copy-button.test.d.ts +2 -0
  6. package/dist/__tests__/copy-button.test.d.ts.map +1 -0
  7. package/dist/__tests__/copy-button.test.js +116 -0
  8. package/dist/__tests__/copy-button.test.js.map +1 -0
  9. package/dist/__tests__/search-palette.test.d.ts +2 -0
  10. package/dist/__tests__/search-palette.test.d.ts.map +1 -0
  11. package/dist/__tests__/search-palette.test.js +71 -0
  12. package/dist/__tests__/search-palette.test.js.map +1 -0
  13. package/dist/__tests__/shiki.test.d.ts +2 -0
  14. package/dist/__tests__/shiki.test.d.ts.map +1 -0
  15. package/dist/__tests__/shiki.test.js +37 -0
  16. package/dist/__tests__/shiki.test.js.map +1 -0
  17. package/dist/__tests__/theme-css.test.d.ts +2 -0
  18. package/dist/__tests__/theme-css.test.d.ts.map +1 -0
  19. package/dist/__tests__/theme-css.test.js +124 -0
  20. package/dist/__tests__/theme-css.test.js.map +1 -0
  21. package/dist/__tests__/theme-helpers.test.d.ts +2 -0
  22. package/dist/__tests__/theme-helpers.test.d.ts.map +1 -0
  23. package/dist/__tests__/theme-helpers.test.js +81 -0
  24. package/dist/__tests__/theme-helpers.test.js.map +1 -0
  25. package/dist/index.d.ts +63 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +13 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/islands/CodeTabs.d.ts +21 -0
  30. package/dist/islands/CodeTabs.d.ts.map +1 -0
  31. package/dist/islands/CodeTabs.js +125 -0
  32. package/dist/islands/CodeTabs.js.map +1 -0
  33. package/dist/islands/CopyButton.d.ts +16 -0
  34. package/dist/islands/CopyButton.d.ts.map +1 -0
  35. package/dist/islands/CopyButton.js +54 -0
  36. package/dist/islands/CopyButton.js.map +1 -0
  37. package/dist/islands/SearchPalette.d.ts +2 -0
  38. package/dist/islands/SearchPalette.d.ts.map +1 -0
  39. package/dist/islands/SearchPalette.js +109 -0
  40. package/dist/islands/SearchPalette.js.map +1 -0
  41. package/dist/islands/SearchResults.d.ts +2 -0
  42. package/dist/islands/SearchResults.d.ts.map +1 -0
  43. package/dist/islands/SearchResults.js +130 -0
  44. package/dist/islands/SearchResults.js.map +1 -0
  45. package/dist/islands/ThemeToggle.d.ts +12 -0
  46. package/dist/islands/ThemeToggle.d.ts.map +1 -0
  47. package/dist/islands/ThemeToggle.js +43 -0
  48. package/dist/islands/ThemeToggle.js.map +1 -0
  49. package/dist/layouts/DocPage.test.d.ts +2 -0
  50. package/dist/layouts/DocPage.test.d.ts.map +1 -0
  51. package/dist/layouts/DocPage.test.js +165 -0
  52. package/dist/layouts/DocPage.test.js.map +1 -0
  53. package/dist/lib/utils.d.ts +10 -0
  54. package/dist/lib/utils.d.ts.map +1 -0
  55. package/dist/lib/utils.js +13 -0
  56. package/dist/lib/utils.js.map +1 -0
  57. package/dist/scripts/code-block-enhancer.d.ts +16 -0
  58. package/dist/scripts/code-block-enhancer.d.ts.map +1 -0
  59. package/dist/scripts/code-block-enhancer.js +55 -0
  60. package/dist/scripts/code-block-enhancer.js.map +1 -0
  61. package/dist/ui/command.d.ts +87 -0
  62. package/dist/ui/command.d.ts.map +1 -0
  63. package/dist/ui/command.js +28 -0
  64. package/dist/ui/command.js.map +1 -0
  65. package/dist/ui/dialog.d.ts +20 -0
  66. package/dist/ui/dialog.d.ts.map +1 -0
  67. package/dist/ui/dialog.js +22 -0
  68. package/dist/ui/dialog.js.map +1 -0
  69. package/dist/utils/parse-highlight-range.d.ts +12 -0
  70. package/dist/utils/parse-highlight-range.d.ts.map +1 -0
  71. package/dist/utils/parse-highlight-range.js +40 -0
  72. package/dist/utils/parse-highlight-range.js.map +1 -0
  73. package/dist/utils/parse-highlight-range.test.d.ts +2 -0
  74. package/dist/utils/parse-highlight-range.test.d.ts.map +1 -0
  75. package/dist/utils/parse-highlight-range.test.js +32 -0
  76. package/dist/utils/parse-highlight-range.test.js.map +1 -0
  77. package/dist/utils/schema-renderer.d.ts +38 -0
  78. package/dist/utils/schema-renderer.d.ts.map +1 -0
  79. package/dist/utils/schema-renderer.js +115 -0
  80. package/dist/utils/schema-renderer.js.map +1 -0
  81. package/dist/utils/schema-renderer.test.d.ts +2 -0
  82. package/dist/utils/schema-renderer.test.d.ts.map +1 -0
  83. package/dist/utils/schema-renderer.test.js +219 -0
  84. package/dist/utils/schema-renderer.test.js.map +1 -0
  85. package/dist/utils/shiki.d.ts +20 -0
  86. package/dist/utils/shiki.d.ts.map +1 -0
  87. package/dist/utils/shiki.js +84 -0
  88. package/dist/utils/shiki.js.map +1 -0
  89. package/dist/utils/sidebar-helpers.d.ts +10 -0
  90. package/dist/utils/sidebar-helpers.d.ts.map +1 -0
  91. package/dist/utils/sidebar-helpers.js +14 -0
  92. package/dist/utils/sidebar-helpers.js.map +1 -0
  93. package/dist/utils/theme-css.d.ts +21 -0
  94. package/dist/utils/theme-css.d.ts.map +1 -0
  95. package/dist/utils/theme-css.js +77 -0
  96. package/dist/utils/theme-css.js.map +1 -0
  97. package/dist/utils/theme-helpers.d.ts +28 -0
  98. package/dist/utils/theme-helpers.d.ts.map +1 -0
  99. package/dist/utils/theme-helpers.js +55 -0
  100. package/dist/utils/theme-helpers.js.map +1 -0
  101. package/dist/utils/toc-helpers.d.ts +12 -0
  102. package/dist/utils/toc-helpers.d.ts.map +1 -0
  103. package/dist/utils/toc-helpers.js +9 -0
  104. package/dist/utils/toc-helpers.js.map +1 -0
  105. package/package.json +68 -0
  106. package/src/components/ApiAuth.astro +116 -0
  107. package/src/components/ApiEndpoint.astro +75 -0
  108. package/src/components/ApiNavigation.astro +110 -0
  109. package/src/components/ApiParameters.astro +204 -0
  110. package/src/components/ApiResponse.astro +144 -0
  111. package/src/components/Callout.astro +54 -0
  112. package/src/components/Card.astro +46 -0
  113. package/src/components/CodeBlock.astro +142 -0
  114. package/src/components/CodeBlockGroup.astro +196 -0
  115. package/src/components/CodeTabs.astro +53 -0
  116. package/src/components/Footer.astro +41 -0
  117. package/src/components/Header.astro +80 -0
  118. package/src/components/Sidebar.astro +117 -0
  119. package/src/components/TabItem.astro +24 -0
  120. package/src/components/TableOfContents.astro +111 -0
  121. package/src/components/Tabs.astro +185 -0
  122. package/src/islands/CodeTabs.tsx +212 -0
  123. package/src/islands/CopyButton.tsx +101 -0
  124. package/src/islands/SearchPalette.tsx +307 -0
  125. package/src/islands/SearchResults.tsx +301 -0
  126. package/src/islands/ThemeToggle.tsx +107 -0
  127. package/src/layouts/ApiReferencePage.astro +239 -0
  128. package/src/layouts/DocPage.astro +199 -0
  129. package/src/layouts/DocPage.test.ts +183 -0
  130. package/src/layouts/LandingPage.astro +143 -0
  131. package/src/lib/utils.ts +13 -0
  132. package/src/styles/global.css +241 -0
  133. package/src/utils/parse-highlight-range.test.ts +40 -0
  134. package/src/utils/parse-highlight-range.ts +41 -0
  135. package/src/utils/schema-renderer.test.ts +269 -0
  136. package/src/utils/schema-renderer.ts +152 -0
  137. package/src/utils/shiki.ts +99 -0
  138. package/src/utils/sidebar-helpers.ts +24 -0
  139. package/src/utils/theme-css.ts +101 -0
  140. package/src/utils/theme-helpers.ts +59 -0
  141. package/src/utils/toc-helpers.ts +11 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=code-tabs.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-tabs.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/code-tabs.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,219 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * CodeTabs island tests.
4
+ *
5
+ * Tests the CodeTabs React island's hook logic for tab switching,
6
+ * localStorage persistence, and cross-group sync via CustomEvent.
7
+ *
8
+ * Following the established pattern (see copy-button.test.ts): we
9
+ * re-implement the hook logic in the test file to avoid JSX rendering
10
+ * issues with Astro React islands in vitest.
11
+ */
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
13
+ import { renderHook, act } from "@testing-library/react";
14
+ import { useState, useEffect, useCallback, useRef } from "react";
15
+ // ---------------------------------------------------------------------------
16
+ // Re-implement the useCodeTabs hook logic for isolated testing
17
+ // SYNC: Must match the hook in CodeTabs.tsx — update both when changing
18
+ // ---------------------------------------------------------------------------
19
+ const LANGUAGE_CHANGE_EVENT = "ndocs-language-change";
20
+ function getStorageKey(syncKey) {
21
+ return `ndocs-preferred-${syncKey}`;
22
+ }
23
+ /**
24
+ * Mirror of the hook logic in CodeTabs.tsx.
25
+ * Must stay in sync with the real implementation.
26
+ */
27
+ function useCodeTabs({ languages, syncKey = "sdk-language" }) {
28
+ const storageKey = getStorageKey(syncKey);
29
+ // Always start at index 0 — SSR-safe. Stored preference restored in useEffect.
30
+ const [activeIndex, setActiveIndex] = useState(0);
31
+ const isInternalChange = useRef(false);
32
+ // Restore stored preference after hydration
33
+ useEffect(() => {
34
+ try {
35
+ const stored = localStorage.getItem(storageKey);
36
+ if (stored) {
37
+ const idx = languages.indexOf(stored);
38
+ if (idx !== -1 && idx !== 0) {
39
+ setActiveIndex(idx);
40
+ }
41
+ }
42
+ }
43
+ catch {
44
+ // localStorage unavailable
45
+ }
46
+ }, [storageKey, languages]);
47
+ const setTab = useCallback((index) => {
48
+ if (index < 0 || index >= languages.length)
49
+ return;
50
+ setActiveIndex(index);
51
+ const language = languages[index];
52
+ if (language) {
53
+ try {
54
+ localStorage.setItem(storageKey, language);
55
+ }
56
+ catch {
57
+ // localStorage unavailable
58
+ }
59
+ isInternalChange.current = true;
60
+ window.dispatchEvent(new CustomEvent(LANGUAGE_CHANGE_EVENT, {
61
+ detail: { syncKey, language },
62
+ }));
63
+ isInternalChange.current = false;
64
+ }
65
+ }, [languages, storageKey, syncKey]);
66
+ useEffect(() => {
67
+ const handler = (e) => {
68
+ if (isInternalChange.current)
69
+ return;
70
+ const detail = e.detail;
71
+ if (detail?.syncKey === syncKey) {
72
+ const idx = languages.indexOf(detail.language);
73
+ if (idx !== -1) {
74
+ setActiveIndex(idx);
75
+ }
76
+ }
77
+ };
78
+ window.addEventListener(LANGUAGE_CHANGE_EVENT, handler);
79
+ return () => window.removeEventListener(LANGUAGE_CHANGE_EVENT, handler);
80
+ }, [languages, syncKey]);
81
+ return { activeIndex, setTab };
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Tests
85
+ // ---------------------------------------------------------------------------
86
+ describe("CodeTabs", () => {
87
+ beforeEach(() => {
88
+ localStorage.clear();
89
+ // Remove any lingering event listeners by replacing window dispatch
90
+ vi.restoreAllMocks();
91
+ });
92
+ afterEach(() => {
93
+ localStorage.clear();
94
+ vi.restoreAllMocks();
95
+ });
96
+ describe("Module exports", () => {
97
+ it("exports CodeTabs as a named export", async () => {
98
+ const mod = await import("../islands/CodeTabs");
99
+ expect(mod.CodeTabs).toBeDefined();
100
+ expect(typeof mod.CodeTabs).toBe("function");
101
+ });
102
+ it("CodeTabs is a function component", async () => {
103
+ const { CodeTabs } = await import("../islands/CodeTabs");
104
+ expect(CodeTabs.name).toBe("CodeTabs");
105
+ });
106
+ });
107
+ describe("useCodeTabs hook — tab switching", () => {
108
+ it("starts at index 0 when no stored preference", () => {
109
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript", "curl"] }));
110
+ expect(result.current.activeIndex).toBe(0);
111
+ });
112
+ it("switches to the selected tab", () => {
113
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript", "curl"] }));
114
+ act(() => {
115
+ result.current.setTab(2);
116
+ });
117
+ expect(result.current.activeIndex).toBe(2);
118
+ });
119
+ it("ignores out-of-bounds setTab (negative)", () => {
120
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript", "curl"] }));
121
+ act(() => {
122
+ result.current.setTab(-1);
123
+ });
124
+ expect(result.current.activeIndex).toBe(0);
125
+ });
126
+ it("ignores out-of-bounds setTab (too high)", () => {
127
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript", "curl"] }));
128
+ act(() => {
129
+ result.current.setTab(99);
130
+ });
131
+ expect(result.current.activeIndex).toBe(0);
132
+ });
133
+ });
134
+ describe("useCodeTabs hook — localStorage persistence", () => {
135
+ it("saves language to localStorage on tab change", () => {
136
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript", "curl"] }));
137
+ act(() => {
138
+ result.current.setTab(1);
139
+ });
140
+ expect(localStorage.getItem("ndocs-preferred-sdk-language")).toBe("javascript");
141
+ });
142
+ it("reads stored preference on mount", () => {
143
+ localStorage.setItem("ndocs-preferred-sdk-language", "curl");
144
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript", "curl"] }));
145
+ expect(result.current.activeIndex).toBe(2);
146
+ });
147
+ it("falls back to index 0 when stored language is not in tabs", () => {
148
+ localStorage.setItem("ndocs-preferred-sdk-language", "ruby");
149
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript", "curl"] }));
150
+ expect(result.current.activeIndex).toBe(0);
151
+ });
152
+ it("supports custom syncKey for separate persistence", () => {
153
+ const { result } = renderHook(() => useCodeTabs({
154
+ languages: ["rest", "graphql"],
155
+ syncKey: "api-style",
156
+ }));
157
+ act(() => {
158
+ result.current.setTab(1);
159
+ });
160
+ expect(localStorage.getItem("ndocs-preferred-api-style")).toBe("graphql");
161
+ // Default key should remain unset
162
+ expect(localStorage.getItem("ndocs-preferred-sdk-language")).toBeNull();
163
+ });
164
+ });
165
+ describe("useCodeTabs hook — cross-group sync", () => {
166
+ it("broadcasts language change via CustomEvent", () => {
167
+ const handler = vi.fn();
168
+ window.addEventListener(LANGUAGE_CHANGE_EVENT, handler);
169
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript"] }));
170
+ act(() => {
171
+ result.current.setTab(1);
172
+ });
173
+ expect(handler).toHaveBeenCalledTimes(1);
174
+ const event = handler.mock.calls[0][0];
175
+ expect(event.detail).toEqual({
176
+ syncKey: "sdk-language",
177
+ language: "javascript",
178
+ });
179
+ window.removeEventListener(LANGUAGE_CHANGE_EVENT, handler);
180
+ });
181
+ it("listens for external language change events", () => {
182
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript", "curl"] }));
183
+ expect(result.current.activeIndex).toBe(0);
184
+ // Simulate an external event (from another CodeTabs instance)
185
+ act(() => {
186
+ window.dispatchEvent(new CustomEvent(LANGUAGE_CHANGE_EVENT, {
187
+ detail: { syncKey: "sdk-language", language: "curl" },
188
+ }));
189
+ });
190
+ expect(result.current.activeIndex).toBe(2);
191
+ });
192
+ it("ignores events with a different syncKey", () => {
193
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript"] }));
194
+ act(() => {
195
+ window.dispatchEvent(new CustomEvent(LANGUAGE_CHANGE_EVENT, {
196
+ detail: { syncKey: "other-key", language: "javascript" },
197
+ }));
198
+ });
199
+ // Should NOT have changed
200
+ expect(result.current.activeIndex).toBe(0);
201
+ });
202
+ it("ignores events for languages not in its tab list", () => {
203
+ const { result } = renderHook(() => useCodeTabs({ languages: ["python", "javascript"] }));
204
+ act(() => {
205
+ window.dispatchEvent(new CustomEvent(LANGUAGE_CHANGE_EVENT, {
206
+ detail: { syncKey: "sdk-language", language: "ruby" },
207
+ }));
208
+ });
209
+ expect(result.current.activeIndex).toBe(0);
210
+ });
211
+ it("cleans up event listener on unmount", () => {
212
+ const spy = vi.spyOn(window, "removeEventListener");
213
+ const { unmount } = renderHook(() => useCodeTabs({ languages: ["python", "javascript"] }));
214
+ unmount();
215
+ expect(spy).toHaveBeenCalledWith(LANGUAGE_CHANGE_EVENT, expect.any(Function));
216
+ });
217
+ });
218
+ });
219
+ //# sourceMappingURL=code-tabs.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-tabs.test.js","sourceRoot":"","sources":["../../src/__tests__/code-tabs.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=copy-button.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-button.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/copy-button.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,116 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
+ import { renderHook, act } from "@testing-library/react";
4
+ import { useState, useCallback, useRef } from "react";
5
+ /**
6
+ * Tests for CopyButton island behavior.
7
+ *
8
+ * Since CopyButton uses navigator.clipboard (browser API), we mock it.
9
+ * We test the core logic: copied state, timeout, aria-label.
10
+ *
11
+ * NOTE: The hook logic is re-implemented here rather than importing the
12
+ * CopyButton component directly. This is intentional — Astro React
13
+ * islands with JSX don't render cleanly in vitest without a full Astro
14
+ * build context. This matches the ThemeToggle test pattern. If the hook
15
+ * logic changes in CopyButton.tsx, these tests must be updated to match.
16
+ */
17
+ // Re-implement the hook logic for isolated testing (avoids JSX rendering issues
18
+ // with the React island in vitest — consistent with the ThemeToggle test pattern)
19
+ function useCopyLogic(code) {
20
+ const [copied, setCopied] = useState(false);
21
+ const timeoutRef = useRef(null);
22
+ const handleCopy = useCallback(() => {
23
+ if (timeoutRef.current) {
24
+ clearTimeout(timeoutRef.current);
25
+ }
26
+ navigator.clipboard.writeText(code).then(() => {
27
+ setCopied(true);
28
+ timeoutRef.current = setTimeout(() => {
29
+ setCopied(false);
30
+ timeoutRef.current = null;
31
+ }, 2000);
32
+ });
33
+ }, [code]);
34
+ return { copied, handleCopy };
35
+ }
36
+ describe("CopyButton logic", () => {
37
+ beforeEach(() => {
38
+ vi.useFakeTimers();
39
+ // Mock clipboard API
40
+ Object.assign(navigator, {
41
+ clipboard: {
42
+ writeText: vi.fn().mockResolvedValue(undefined),
43
+ },
44
+ });
45
+ });
46
+ afterEach(() => {
47
+ vi.useRealTimers();
48
+ vi.restoreAllMocks();
49
+ });
50
+ it("starts with copied = false", () => {
51
+ const { result } = renderHook(() => useCopyLogic("hello"));
52
+ expect(result.current.copied).toBe(false);
53
+ });
54
+ it("sets copied = true after handleCopy", async () => {
55
+ const { result } = renderHook(() => useCopyLogic("hello"));
56
+ await act(async () => {
57
+ result.current.handleCopy();
58
+ // Flush the promise
59
+ await Promise.resolve();
60
+ });
61
+ expect(result.current.copied).toBe(true);
62
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith("hello");
63
+ });
64
+ it("resets copied = false after 2 seconds", async () => {
65
+ const { result } = renderHook(() => useCopyLogic("hello"));
66
+ await act(async () => {
67
+ result.current.handleCopy();
68
+ await Promise.resolve();
69
+ });
70
+ expect(result.current.copied).toBe(true);
71
+ act(() => {
72
+ vi.advanceTimersByTime(2000);
73
+ });
74
+ expect(result.current.copied).toBe(false);
75
+ });
76
+ it("copies the exact code string provided", async () => {
77
+ const code = 'const x = "multiline\\ncode";';
78
+ const { result } = renderHook(() => useCopyLogic(code));
79
+ await act(async () => {
80
+ result.current.handleCopy();
81
+ await Promise.resolve();
82
+ });
83
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(code);
84
+ });
85
+ it("resets timeout on rapid successive clicks", async () => {
86
+ const { result } = renderHook(() => useCopyLogic("hello"));
87
+ // First click
88
+ await act(async () => {
89
+ result.current.handleCopy();
90
+ await Promise.resolve();
91
+ });
92
+ expect(result.current.copied).toBe(true);
93
+ // Advance 1 second (not yet 2s)
94
+ act(() => {
95
+ vi.advanceTimersByTime(1000);
96
+ });
97
+ // Second click — should reset timer
98
+ await act(async () => {
99
+ result.current.handleCopy();
100
+ await Promise.resolve();
101
+ });
102
+ expect(result.current.copied).toBe(true);
103
+ // Advance 1.5 seconds — the first timeout would have expired by now,
104
+ // but the second click reset it
105
+ act(() => {
106
+ vi.advanceTimersByTime(1500);
107
+ });
108
+ expect(result.current.copied).toBe(true);
109
+ // Full 2s from second click
110
+ act(() => {
111
+ vi.advanceTimersByTime(500);
112
+ });
113
+ expect(result.current.copied).toBe(false);
114
+ });
115
+ });
116
+ //# sourceMappingURL=copy-button.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-button.test.js","sourceRoot":"","sources":["../../src/__tests__/copy-button.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search-palette.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-palette.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/search-palette.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * SearchPalette component tests.
3
+ *
4
+ * Tests the SearchPalette island's rendering, trigger button,
5
+ * and keyboard shortcut behavior. Pagefind is mocked since it
6
+ * requires a build-time generated index.
7
+ */
8
+ import { describe, it, expect, vi } from "vitest";
9
+ // Mock Pagefind — it's loaded dynamically and won't exist in test env
10
+ vi.mock("/pagefind/pagefind.js", () => ({
11
+ init: vi.fn(),
12
+ search: vi.fn().mockResolvedValue({ results: [] }),
13
+ }));
14
+ describe("SearchPalette", () => {
15
+ describe("Module exports", () => {
16
+ it("exports SearchPalette as a named export", async () => {
17
+ const mod = await import("../islands/SearchPalette");
18
+ expect(mod.SearchPalette).toBeDefined();
19
+ expect(typeof mod.SearchPalette).toBe("function");
20
+ });
21
+ });
22
+ describe("Component structure", () => {
23
+ it("SearchPalette is a function component", async () => {
24
+ const { SearchPalette } = await import("../islands/SearchPalette");
25
+ // React function components have a name
26
+ expect(SearchPalette.name).toBe("SearchPalette");
27
+ });
28
+ });
29
+ describe("Constants", () => {
30
+ it("uses expected debounce and result limits", async () => {
31
+ // Verify the source contains expected constants by importing the module
32
+ const mod = await import("../islands/SearchPalette");
33
+ // The component exists and is callable — constants are internal
34
+ expect(mod.SearchPalette).toBeDefined();
35
+ });
36
+ });
37
+ });
38
+ describe("ui/command", () => {
39
+ it("exports all expected shadcn Command components", async () => {
40
+ const mod = await import("../ui/command");
41
+ expect(mod.Command).toBeDefined();
42
+ expect(mod.CommandDialog).toBeDefined();
43
+ expect(mod.CommandInput).toBeDefined();
44
+ expect(mod.CommandList).toBeDefined();
45
+ expect(mod.CommandEmpty).toBeDefined();
46
+ expect(mod.CommandGroup).toBeDefined();
47
+ expect(mod.CommandItem).toBeDefined();
48
+ expect(mod.CommandShortcut).toBeDefined();
49
+ expect(mod.CommandSeparator).toBeDefined();
50
+ });
51
+ it("CommandDialog is a function (not forwardRef wrapper)", async () => {
52
+ const { CommandDialog } = await import("../ui/command");
53
+ expect(typeof CommandDialog).toBe("function");
54
+ });
55
+ });
56
+ describe("ui/dialog", () => {
57
+ it("exports all expected shadcn Dialog components", async () => {
58
+ const mod = await import("../ui/dialog");
59
+ expect(mod.Dialog).toBeDefined();
60
+ expect(mod.DialogPortal).toBeDefined();
61
+ expect(mod.DialogOverlay).toBeDefined();
62
+ expect(mod.DialogClose).toBeDefined();
63
+ expect(mod.DialogTrigger).toBeDefined();
64
+ expect(mod.DialogContent).toBeDefined();
65
+ expect(mod.DialogHeader).toBeDefined();
66
+ expect(mod.DialogFooter).toBeDefined();
67
+ expect(mod.DialogTitle).toBeDefined();
68
+ expect(mod.DialogDescription).toBeDefined();
69
+ });
70
+ });
71
+ //# sourceMappingURL=search-palette.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-palette.test.js","sourceRoot":"","sources":["../../src/__tests__/search-palette.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=shiki.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shiki.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/shiki.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { highlight } from "../utils/shiki.js";
3
+ describe("highlight utility", () => {
4
+ it("returns HTML string with CSS variables for dual themes", async () => {
5
+ const html = await highlight("const x = 1;", "typescript");
6
+ // Output should contain Shiki dual-theme CSS variables
7
+ expect(html).toContain("--shiki-light");
8
+ expect(html).toContain("--shiki-dark");
9
+ // Should be wrapped in <pre><code>
10
+ expect(html).toContain("<pre");
11
+ expect(html).toContain("<code");
12
+ });
13
+ it("falls back to plaintext for unknown languages", async () => {
14
+ // Should not throw — falls back to "text"
15
+ const html = await highlight("hello world", "nonexistent-language");
16
+ expect(html).toContain("hello world");
17
+ expect(html).toContain("<pre");
18
+ });
19
+ it("applies line highlighting decorations", async () => {
20
+ const code = "line1\nline2\nline3";
21
+ const html = await highlight(code, "text", { highlightLines: [2] });
22
+ // Line 2 should have the "highlighted" class
23
+ expect(html).toContain("highlighted");
24
+ });
25
+ it("handles empty code string", async () => {
26
+ const html = await highlight("", "text");
27
+ expect(html).toContain("<pre");
28
+ });
29
+ it("reuses cached highlighter across calls", async () => {
30
+ // Two calls should work without error — verifying cache
31
+ const html1 = await highlight("const a = 1;", "typescript");
32
+ const html2 = await highlight("const b = 2;", "typescript");
33
+ expect(html1).toContain("--shiki-light");
34
+ expect(html2).toContain("--shiki-light");
35
+ });
36
+ });
37
+ //# sourceMappingURL=shiki.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shiki.test.js","sourceRoot":"","sources":["../../src/__tests__/shiki.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAkB,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QAE3D,uDAAuD;QACvD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACvC,mCAAmC;QACnC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,0CAA0C;QAC1C,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,aAAa,EAAE,sBAAsB,CAAC,CAAC;QACpE,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,IAAI,GAAG,qBAAqB,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAEpE,6CAA6C;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;QACzC,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,wDAAwD;QACxD,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QAC5D,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=theme-css.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-css.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/theme-css.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { generateThemeCSS } from "../utils/theme-css.js";
3
+ describe("generateThemeCSS", () => {
4
+ it("returns empty string when no theme config is set", () => {
5
+ const config = {};
6
+ expect(generateThemeCSS(config)).toBe("");
7
+ });
8
+ it("returns empty string when theme exists but no values are set", () => {
9
+ const config = { theme: {} };
10
+ expect(generateThemeCSS(config)).toBe("");
11
+ });
12
+ it("generates --color-primary from primaryColor", () => {
13
+ const config = { theme: { primaryColor: "#3b82f6" } };
14
+ const css = generateThemeCSS(config);
15
+ expect(css).toContain("--color-primary: #3b82f6;");
16
+ expect(css).toMatch(/^:root \{/);
17
+ });
18
+ it("generates font overrides from fontFamily and codeFontFamily", () => {
19
+ const config = {
20
+ theme: { fontFamily: "'Poppins', sans-serif", codeFontFamily: "'Fira Code', monospace" },
21
+ };
22
+ const css = generateThemeCSS(config);
23
+ expect(css).toContain("--font-sans: 'Poppins', sans-serif;");
24
+ expect(css).toContain("--font-mono: 'Fira Code', monospace;");
25
+ });
26
+ it("maps string color values to -dark variants (backward compat)", () => {
27
+ const config = {
28
+ theme: {
29
+ colors: {
30
+ primary: "oklch(0.7 0.15 250)",
31
+ background: "#0a0a0a",
32
+ foreground: "#fafafa",
33
+ muted: "#a1a1aa",
34
+ border: "#27272a",
35
+ },
36
+ },
37
+ };
38
+ const css = generateThemeCSS(config);
39
+ expect(css).toContain("--color-primary: oklch(0.7 0.15 250);");
40
+ // String values → dark-only overrides
41
+ expect(css).toContain("--color-surface-dark: #0a0a0a;");
42
+ expect(css).toContain("--color-text-dark: #fafafa;");
43
+ expect(css).toContain("--color-text-muted-dark: #a1a1aa;");
44
+ expect(css).toContain("--color-border-dark: #27272a;");
45
+ // Should NOT set base (light) tokens
46
+ expect(css).not.toMatch(/--color-surface: /);
47
+ expect(css).not.toMatch(/--color-text: /);
48
+ });
49
+ it("maps { light, dark } pairs to both base and -dark variants", () => {
50
+ const config = {
51
+ theme: {
52
+ colors: {
53
+ background: { light: "#ffffff", dark: "#0a0a0a" },
54
+ foreground: { light: "#1f2937", dark: "#fafafa" },
55
+ },
56
+ },
57
+ };
58
+ const css = generateThemeCSS(config);
59
+ // Light values → base tokens
60
+ expect(css).toContain("--color-surface: #ffffff;");
61
+ expect(css).toContain("--color-text: #1f2937;");
62
+ // Dark values → -dark variants
63
+ expect(css).toContain("--color-surface-dark: #0a0a0a;");
64
+ expect(css).toContain("--color-text-dark: #fafafa;");
65
+ });
66
+ it("handles partial pairs (dark-only object)", () => {
67
+ const config = {
68
+ theme: {
69
+ colors: {
70
+ background: { dark: "#0a0a0a" },
71
+ },
72
+ },
73
+ };
74
+ const css = generateThemeCSS(config);
75
+ expect(css).toContain("--color-surface-dark: #0a0a0a;");
76
+ expect(css).not.toMatch(/--color-surface: /);
77
+ });
78
+ it("handles partial pairs (light-only object)", () => {
79
+ const config = {
80
+ theme: {
81
+ colors: {
82
+ background: { light: "#ffffff" },
83
+ },
84
+ },
85
+ };
86
+ const css = generateThemeCSS(config);
87
+ expect(css).toContain("--color-surface: #ffffff;");
88
+ expect(css).not.toContain("--color-surface-dark");
89
+ });
90
+ it("prefers primaryColor over colors.primary (shorthand wins)", () => {
91
+ const config = {
92
+ theme: {
93
+ primaryColor: "#direct",
94
+ colors: { primary: "#nested" },
95
+ },
96
+ };
97
+ const css = generateThemeCSS(config);
98
+ expect(css).toContain("--color-primary: #direct;");
99
+ expect(css).not.toContain("#nested");
100
+ });
101
+ it("falls back to colors.primary when primaryColor is not set", () => {
102
+ const config = {
103
+ theme: { colors: { primary: "#nested" } },
104
+ };
105
+ const css = generateThemeCSS(config);
106
+ expect(css).toContain("--color-primary: #nested;");
107
+ });
108
+ it("maps accentColor to --color-primary-light", () => {
109
+ const config = { theme: { accentColor: "#8b5cf6" } };
110
+ const css = generateThemeCSS(config);
111
+ expect(css).toContain("--color-primary-light: #8b5cf6;");
112
+ });
113
+ it("only includes properties that have values", () => {
114
+ const config = {
115
+ theme: { primaryColor: "#3b82f6" },
116
+ };
117
+ const css = generateThemeCSS(config);
118
+ // Should NOT include font or other missing properties
119
+ expect(css).not.toContain("--font-sans");
120
+ expect(css).not.toContain("--color-surface-dark");
121
+ expect(css).not.toContain("--color-surface:");
122
+ });
123
+ });
124
+ //# sourceMappingURL=theme-css.test.js.map