@panyam/tsappkit 0.0.3 → 0.0.5

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.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * ActionCompiler - Shared action code compilation and execution
3
+ * Used by docs components like ExampleRunner and PlaygroundPage
4
+ */
5
+
6
+ import ace from "ace-builds";
7
+
8
+ export interface ActionCompileResult {
9
+ success: boolean;
10
+ fn: ((node: any) => any) | null;
11
+ error: string | null;
12
+ line: number | null; // 0-indexed line number for error
13
+ }
14
+
15
+ export interface ActionRunResult {
16
+ success: boolean;
17
+ result: any;
18
+ error: string | null;
19
+ line: number | null; // 0-indexed line number for runtime error
20
+ }
21
+
22
+ export class ActionCompiler {
23
+ private editor: ace.Ace.Editor | null = null;
24
+ private compiledFn: ((node: any) => any) | null = null;
25
+ private compileError: string | null = null;
26
+ private statusEl: HTMLElement | null = null;
27
+ private onCompile: ((result: ActionCompileResult) => void) | null = null;
28
+
29
+ constructor(
30
+ editor: ace.Ace.Editor,
31
+ statusEl: HTMLElement | null = null,
32
+ onCompile: ((result: ActionCompileResult) => void) | null = null,
33
+ ) {
34
+ this.editor = editor;
35
+ this.statusEl = statusEl;
36
+ this.onCompile = onCompile;
37
+
38
+ // Setup change listener with debounce
39
+ let compileTimeout: number | null = null;
40
+ this.editor.session.on("change", () => {
41
+ if (compileTimeout) clearTimeout(compileTimeout);
42
+ compileTimeout = window.setTimeout(() => this.compile(), 300);
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Compile the current editor content
48
+ */
49
+ compile(): ActionCompileResult {
50
+ if (!this.editor) {
51
+ return { success: false, fn: null, error: "No editor", line: null };
52
+ }
53
+
54
+ const code = this.editor.getValue().trim();
55
+ const session = this.editor.session;
56
+
57
+ // Clear previous annotations
58
+ session.clearAnnotations();
59
+ this.compiledFn = null;
60
+ this.compileError = null;
61
+
62
+ if (!code) {
63
+ this.updateStatus("", "");
64
+ const result = { success: true, fn: null, error: null, line: null };
65
+ this.onCompile?.(result);
66
+ return result;
67
+ }
68
+
69
+ try {
70
+ // Compile using new Function
71
+ this.compiledFn = new Function("node", code) as (node: any) => any;
72
+ this.updateStatus("✓", "success");
73
+ const result = { success: true, fn: this.compiledFn, error: null, line: null };
74
+ this.onCompile?.(result);
75
+ return result;
76
+ } catch (e: any) {
77
+ this.compileError = e.message;
78
+ this.updateStatus("✗ Compile Error", "error");
79
+
80
+ // Try to extract line number from error message
81
+ // Common patterns: "at line 5", "line 5", ":5:", "(5:"
82
+ const lineMatch = e.message.match(/(?:at line |line |:|\()(\d+)(?::|,|\))/i);
83
+ const line = lineMatch ? parseInt(lineMatch[1], 10) - 1 : 0; // 0-indexed
84
+
85
+ session.setAnnotations([
86
+ {
87
+ row: line,
88
+ column: 0,
89
+ text: e.message,
90
+ type: "error",
91
+ },
92
+ ]);
93
+
94
+ const result = { success: false, fn: null, error: e.message, line };
95
+ this.onCompile?.(result);
96
+ return result;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Run the compiled action on a parse tree node
102
+ */
103
+ run(node: any): ActionRunResult {
104
+ if (!node) {
105
+ return { success: false, result: null, error: "No parse result available", line: null };
106
+ }
107
+
108
+ if (this.compileError) {
109
+ return { success: false, result: null, error: `Compile Error: ${this.compileError}`, line: null };
110
+ }
111
+
112
+ if (!this.compiledFn) {
113
+ return { success: true, result: null, error: null, line: null }; // No action code
114
+ }
115
+
116
+ try {
117
+ const result = this.compiledFn(node);
118
+ // Clear runtime error annotations
119
+ if (this.editor) {
120
+ this.editor.session.clearAnnotations();
121
+ }
122
+ this.updateStatus("✓", "success");
123
+ return { success: true, result, error: null, line: null };
124
+ } catch (e: any) {
125
+ // Try to extract line number from runtime error stack
126
+ const stack = e.stack || "";
127
+ const lineMatch = stack.match(/<anonymous>:(\d+):/);
128
+ const line = lineMatch ? parseInt(lineMatch[1], 10) - 1 : null; // 0-indexed
129
+
130
+ // Set runtime error annotation
131
+ if (this.editor && line !== null) {
132
+ this.editor.session.setAnnotations([
133
+ {
134
+ row: line,
135
+ column: 0,
136
+ text: e.message,
137
+ type: "error",
138
+ },
139
+ ]);
140
+ }
141
+
142
+ this.updateStatus("✗ Runtime Error", "error");
143
+ return { success: false, result: null, error: e.message, line };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Get the current compile error (if any)
149
+ */
150
+ getCompileError(): string | null {
151
+ return this.compileError;
152
+ }
153
+
154
+ /**
155
+ * Check if there's a compiled action function
156
+ */
157
+ hasCompiledAction(): boolean {
158
+ return this.compiledFn !== null;
159
+ }
160
+
161
+ /**
162
+ * Update status element
163
+ */
164
+ private updateStatus(text: string, statusClass: string): void {
165
+ if (this.statusEl) {
166
+ this.statusEl.textContent = text;
167
+ // Clear and set status classes without removing base class
168
+ this.statusEl.classList.remove("success", "error");
169
+ if (statusClass) {
170
+ this.statusEl.classList.add(statusClass);
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Clear annotations and status
177
+ */
178
+ clear(): void {
179
+ if (this.editor) {
180
+ this.editor.session.clearAnnotations();
181
+ }
182
+ this.compiledFn = null;
183
+ this.compileError = null;
184
+ this.updateStatus("", "");
185
+ }
186
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * CodeBlock.ts - Enhances pre/code blocks with copy functionality
3
+ * Automatically finds all pre>code elements and adds a copy button
4
+ */
5
+
6
+ export class CodeBlock {
7
+ private container: HTMLElement;
8
+ private copyButton: HTMLButtonElement;
9
+ private codeElement: HTMLElement;
10
+
11
+ constructor(preElement: HTMLPreElement) {
12
+ this.codeElement = preElement.querySelector('code') || preElement;
13
+
14
+ // Wrap pre in a container for positioning
15
+ this.container = document.createElement('div');
16
+ this.container.className = 'code-block';
17
+ preElement.parentNode?.insertBefore(this.container, preElement);
18
+ this.container.appendChild(preElement);
19
+
20
+ // Create copy button
21
+ this.copyButton = document.createElement('button');
22
+ this.copyButton.className = 'code-block-copy';
23
+ this.copyButton.setAttribute('aria-label', 'Copy code');
24
+ this.copyButton.innerHTML = this.getCopyIcon();
25
+ this.copyButton.addEventListener('click', () => this.copyCode());
26
+
27
+ this.container.appendChild(this.copyButton);
28
+ }
29
+
30
+ private getCopyIcon(): string {
31
+ return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
32
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
33
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
34
+ </svg>`;
35
+ }
36
+
37
+ private getCheckIcon(): string {
38
+ return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
39
+ <polyline points="20 6 9 17 4 12"></polyline>
40
+ </svg>`;
41
+ }
42
+
43
+ private async copyCode(): Promise<void> {
44
+ const text = this.codeElement.textContent || '';
45
+
46
+ try {
47
+ await navigator.clipboard.writeText(text);
48
+ this.showSuccess();
49
+ } catch (err) {
50
+ // Fallback for older browsers
51
+ this.fallbackCopy(text);
52
+ }
53
+ }
54
+
55
+ private fallbackCopy(text: string): void {
56
+ const textarea = document.createElement('textarea');
57
+ textarea.value = text;
58
+ textarea.style.position = 'fixed';
59
+ textarea.style.opacity = '0';
60
+ document.body.appendChild(textarea);
61
+ textarea.select();
62
+
63
+ try {
64
+ document.execCommand('copy');
65
+ this.showSuccess();
66
+ } catch (err) {
67
+ console.error('Failed to copy:', err);
68
+ }
69
+
70
+ document.body.removeChild(textarea);
71
+ }
72
+
73
+ private showSuccess(): void {
74
+ this.copyButton.innerHTML = this.getCheckIcon();
75
+ this.copyButton.classList.add('copied');
76
+
77
+ setTimeout(() => {
78
+ this.copyButton.innerHTML = this.getCopyIcon();
79
+ this.copyButton.classList.remove('copied');
80
+ }, 2000);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Initialize all code blocks on the page
86
+ * @param selector - CSS selector for finding pre elements (default: '.content-body pre')
87
+ */
88
+ export function initCodeBlocks(selector: string = '.content-body pre'): void {
89
+ const preElements = document.querySelectorAll(selector);
90
+
91
+ preElements.forEach((pre) => {
92
+ // Skip if already initialized
93
+ if (pre.parentElement?.classList.contains('code-block')) {
94
+ return;
95
+ }
96
+ // Skip if this pre contains another pre (outer wrapper of double-wrapped code blocks)
97
+ // We only want to process the innermost pre that has the actual code
98
+ if (pre.querySelector('pre')) {
99
+ return;
100
+ }
101
+ new CodeBlock(pre as HTMLPreElement);
102
+ });
103
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Documentation site components and utilities
3
+ *
4
+ * This module provides reusable components for building documentation sites:
5
+ * - CodeBlock: Copy-to-clipboard for code blocks
6
+ * - pageSetup: Common page initialization (sidebar highlighting, etc.)
7
+ * - ActionCompiler: Compile and run user-provided action code
8
+ */
9
+
10
+ export { CodeBlock, initCodeBlocks } from './CodeBlock';
11
+ export { highlightActiveSidebarLink, initPageSetup } from './pageSetup';
12
+ export { ActionCompiler } from './ActionCompiler';
13
+ export type { ActionCompileResult, ActionRunResult } from './ActionCompiler';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * pageSetup.ts - Common page initialization shared across all doc pages
3
+ */
4
+
5
+ /**
6
+ * Highlights the currently active link in the sidebar navigation
7
+ * @param sidebarSelector - CSS selector for sidebar links (default: '.sidebar .section-nav a')
8
+ */
9
+ export function highlightActiveSidebarLink(sidebarSelector: string = '.sidebar .section-nav a'): void {
10
+ const currentPath = window.location.pathname;
11
+ const sidebarLinks = document.querySelectorAll(sidebarSelector);
12
+
13
+ sidebarLinks.forEach((link) => {
14
+ const href = link.getAttribute('href');
15
+ if (href && currentPath.endsWith(href.replace(/\/$/, '') + '/')) {
16
+ link.classList.add('active');
17
+ } else if (href && currentPath === href) {
18
+ link.classList.add('active');
19
+ }
20
+ });
21
+ }
22
+
23
+ /**
24
+ * Initialize common page features
25
+ * Call this on DOMContentLoaded for all doc pages
26
+ * @param options - Configuration options for page setup
27
+ */
28
+ export function initPageSetup(options: {
29
+ sidebarSelector?: string;
30
+ } = {}): void {
31
+ highlightActiveSidebarLink(options.sidebarSelector);
32
+ }
@@ -0,0 +1,22 @@
1
+ // ============================================
2
+ // DocsPage - Main Documentation Styles
3
+ //
4
+ // This is the main entry point for documentation site styling.
5
+ // Import this file in your project's main stylesheet:
6
+ //
7
+ // @use '@panyam/tsappkit/styles/docs/DocsPage.scss';
8
+ //
9
+ // Or import individual partials for customization:
10
+ //
11
+ // @use '@panyam/tsappkit/styles/docs/variables';
12
+ // @use '@panyam/tsappkit/styles/docs/base';
13
+ // // ... add your customizations
14
+ // ============================================
15
+
16
+ @use 'variables';
17
+ @use 'base';
18
+ @use 'header';
19
+ @use 'layout';
20
+ @use 'content';
21
+ @use 'footer';
22
+ @use 'responsive';
@@ -0,0 +1,39 @@
1
+ // ============================================
2
+ // Base Styles - Reset & Typography
3
+ // ============================================
4
+
5
+ // Reset
6
+ *,
7
+ *::before,
8
+ *::after {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ html {
15
+ font-size: 16px;
16
+ scroll-behavior: smooth;
17
+ }
18
+
19
+ body {
20
+ font-family: var(--font-sans);
21
+ color: var(--color-text);
22
+ background: var(--color-bg);
23
+ line-height: 1.6;
24
+ min-height: 100vh;
25
+ }
26
+
27
+ a {
28
+ color: var(--color-primary);
29
+ text-decoration: none;
30
+
31
+ &:hover {
32
+ text-decoration: underline;
33
+ }
34
+ }
35
+
36
+ code,
37
+ pre {
38
+ font-family: var(--font-mono);
39
+ }
@@ -0,0 +1,122 @@
1
+ // ============================================
2
+ // Content Styles
3
+ // ============================================
4
+
5
+ .content-header {
6
+ margin-bottom: var(--spacing-xl);
7
+
8
+ h1 {
9
+ font-size: 2.5rem;
10
+ font-weight: 700;
11
+ line-height: 1.2;
12
+ margin-bottom: var(--spacing-sm);
13
+ }
14
+
15
+ .lead {
16
+ font-size: 1.25rem;
17
+ color: var(--color-text-muted);
18
+ }
19
+ }
20
+
21
+ .content-body {
22
+ font-size: 1rem;
23
+ line-height: 1.75;
24
+
25
+ h2 {
26
+ font-size: 1.75rem;
27
+ font-weight: 600;
28
+ margin-top: var(--spacing-xl);
29
+ margin-bottom: var(--spacing-md);
30
+ }
31
+
32
+ h3 {
33
+ font-size: 1.25rem;
34
+ font-weight: 600;
35
+ margin-top: var(--spacing-lg);
36
+ margin-bottom: var(--spacing-sm);
37
+ }
38
+
39
+ p {
40
+ margin-bottom: var(--spacing-md);
41
+ }
42
+
43
+ ul,
44
+ ol {
45
+ margin-bottom: var(--spacing-md);
46
+ padding-left: var(--spacing-lg);
47
+ }
48
+
49
+ li {
50
+ margin-bottom: var(--spacing-xs);
51
+ }
52
+
53
+ // Code blocks
54
+ pre {
55
+ background: var(--color-bg-code);
56
+ color: var(--color-code-text);
57
+ padding: var(--spacing-md);
58
+ border-radius: var(--radius-md);
59
+ overflow-x: auto;
60
+ margin-bottom: var(--spacing-md);
61
+ }
62
+
63
+ // Inline code
64
+ code {
65
+ background: var(--color-bg-alt);
66
+ padding: 0.2em 0.4em;
67
+ border-radius: var(--radius-sm);
68
+ font-size: 0.9em;
69
+ }
70
+
71
+ pre code {
72
+ background: none;
73
+ padding: 0;
74
+ }
75
+ }
76
+
77
+ // Code block with copy button (used with CodeBlock.ts component)
78
+ .code-block {
79
+ position: relative;
80
+ margin-bottom: var(--spacing-md);
81
+
82
+ pre {
83
+ margin-bottom: 0;
84
+ }
85
+ }
86
+
87
+ .code-block-copy {
88
+ position: absolute;
89
+ top: var(--spacing-sm);
90
+ right: var(--spacing-sm);
91
+ padding: 6px 8px;
92
+ background: var(--color-bg-alt);
93
+ border: 1px solid var(--color-border);
94
+ border-radius: var(--radius-sm);
95
+ color: var(--color-text-muted);
96
+ cursor: pointer;
97
+ opacity: 0;
98
+ transition: opacity 0.2s, background 0.2s, color 0.2s;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+
103
+ svg {
104
+ width: 16px;
105
+ height: 16px;
106
+ }
107
+ }
108
+
109
+ .code-block:hover .code-block-copy {
110
+ opacity: 1;
111
+ }
112
+
113
+ .code-block-copy:hover {
114
+ background: var(--color-border);
115
+ color: var(--color-text);
116
+ }
117
+
118
+ .code-block-copy.copied {
119
+ color: #4ade80;
120
+ background: rgba(74, 222, 128, 0.2);
121
+ border-color: rgba(74, 222, 128, 0.3);
122
+ }
@@ -0,0 +1,56 @@
1
+ // ============================================
2
+ // Footer Styles
3
+ // ============================================
4
+
5
+ .site-footer {
6
+ background: var(--color-bg-alt);
7
+ border-top: 1px solid var(--color-border);
8
+ padding: var(--spacing-xl) 0;
9
+ margin-top: var(--spacing-xl);
10
+ }
11
+
12
+ .footer-container {
13
+ max-width: var(--page-max-width);
14
+ margin: 0 auto;
15
+ padding: 0 var(--spacing-lg);
16
+ display: grid;
17
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
18
+ gap: var(--spacing-xl);
19
+ }
20
+
21
+ .footer-section {
22
+ h4 {
23
+ font-weight: 600;
24
+ margin-bottom: var(--spacing-md);
25
+ }
26
+
27
+ ul {
28
+ list-style: none;
29
+ }
30
+
31
+ li {
32
+ margin-bottom: var(--spacing-xs);
33
+ }
34
+
35
+ a {
36
+ color: var(--color-text-muted);
37
+
38
+ &:hover {
39
+ color: var(--color-primary);
40
+ }
41
+ }
42
+
43
+ .version {
44
+ color: var(--color-text-light);
45
+ font-size: 0.875rem;
46
+ }
47
+ }
48
+
49
+ .footer-bottom {
50
+ text-align: center;
51
+ padding-top: var(--spacing-lg);
52
+ margin-top: var(--spacing-lg);
53
+ border-top: 1px solid var(--color-border);
54
+ color: var(--color-text-light);
55
+ font-size: 0.875rem;
56
+ }