Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • grady-corp/grady
1 result
Show changes
Commits on Source (2)
Showing with 385 additions and 197 deletions
......@@ -56,6 +56,7 @@
"tailwindcss": "^4.0.13",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1",
"typescript-svelte-plugin": "^0.3.46",
"vite": "^6.2.1",
"vitest": "^3.0.8",
"zod": "^3.24.2",
......@@ -597,6 +598,8 @@
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
......@@ -847,6 +850,8 @@
"loupe": ["loupe@3.1.3", "", {}, "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug=="],
"lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="],
"lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
"lucide-svelte": ["lucide-svelte@0.479.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-epCj6WL86ykxg7oCQTmPEth5e11pwJUzIfG9ROUsWsTP+WPtb3qat+VmAjfx/r4TRW7memTFcbTPvMrZvKthqw=="],
......@@ -897,6 +902,8 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
......@@ -923,6 +930,8 @@
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
......@@ -1107,6 +1116,8 @@
"svelte-eslint-parser": ["svelte-eslint-parser@1.0.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-JjdEMXOJqy+dxeaElxbN+meTOtVpHfLnq9VGpiTAOLgM0uHO+ogmUsA3IFgx0x3Wl15pqTZWycCikcD7cAQN/g=="],
"svelte2tsx": ["svelte2tsx@0.7.35", "", { "dependencies": { "dedent-js": "^1.0.1", "pascal-case": "^3.1.1" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0" } }, "sha512-z2lnOnrfb5nrlRfFQI8Qdz03xQqMHUfPj0j8l/fQuydrH89cCeN+v9jgDwK9GyMtdTRUkE7Neu9Gh+vfXJAfuQ=="],
"sveltekit-superforms": ["sveltekit-superforms@2.24.0", "", { "dependencies": { "devalue": "^5.1.1", "memoize-weak": "^1.0.2", "ts-deepmerge": "^7.0.2" }, "optionalDependencies": { "@exodus/schemasafe": "^1.3.0", "@gcornut/valibot-json-schema": "^0.31.0", "@sinclair/typebox": "^0.34.28", "@typeschema/class-validator": "^0.3.0", "@vinejs/vine": "^3.0.0", "arktype": "^2.1.9", "class-validator": "^0.14.1", "effect": "^3.13.7", "joi": "^17.13.3", "json-schema-to-ts": "^3.1.1", "superstruct": "^2.0.2", "valibot": "1.0.0-rc.3", "yup": "^1.6.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.3" }, "peerDependencies": { "@sveltejs/kit": "1.x || 2.x", "svelte": "3.x || 4.x || >=5.0.0-next.51" } }, "sha512-JuuaaPDn9OHUKc0Uy8jzv1jUZNfO4AHUE0JLcXjiuJNRokYLqC+RsPDL4/jUkqia97aZzrfTgB/meQ8iS5nNJg=="],
"tailwind-scrollbar": ["tailwind-scrollbar@4.0.1", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-j2ZfUI7p8xmSQdlqaCxEb4Mha8ErvWjDVyu2Ke4IstWprQ/6TmIz1GSLE62vsTlXwnMLYhuvbFbIFzaJGOGtMg=="],
......@@ -1169,6 +1180,8 @@
"typescript-eslint": ["typescript-eslint@8.26.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.26.1", "@typescript-eslint/parser": "8.26.1", "@typescript-eslint/utils": "8.26.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg=="],
"typescript-svelte-plugin": ["typescript-svelte-plugin@0.3.46", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "svelte2tsx": "~0.7.35" } }, "sha512-6GUb+nafp00/WEOKztBddyCsrJNHbIqO32kPfhDFybWqhj89AxUhwedVE2g4rug2E48d40AgG2jLYW611esJAg=="],
"undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
......
......@@ -18,10 +18,10 @@
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1741953770,
"lastModified": 1742147141,
"owner": "cachix",
"repo": "devenv",
"rev": "984272189d4c23e3663a4d7e83b3cf3a532aa53d",
"rev": "e1eb23d427a3a0871c277268a28163898fd37266",
"type": "github"
},
"original": {
......@@ -40,10 +40,10 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1741934023,
"lastModified": 1742106676,
"owner": "nix-community",
"repo": "fenix",
"rev": "4f956eacc9ec619bcd98f4580c663a8749978cc8",
"rev": "9906c9f9a474f189df09629cf2813e8888d42429",
"type": "github"
},
"original": {
......@@ -123,10 +123,10 @@
]
},
"locked": {
"lastModified": 1741379162,
"lastModified": 1742058297,
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "b5a62751225b2f62ff3147d0a334055ebadcd5cc",
"rev": "59f17850021620cd348ad2e9c0c64f4e6325ce2a",
"type": "github"
},
"original": {
......@@ -221,10 +221,10 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1741969460,
"lastModified": 1742072093,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "68612419aa6c9fd5b178b81e6fabbdf46d300ea4",
"rev": "f182029bf7f08a57762b4c762d0917b6803ceff4",
"type": "github"
},
"original": {
......@@ -251,10 +251,10 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1741895161,
"lastModified": 1742071726,
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "185f9deb452760f3abc2fde0500398e3198678cd",
"rev": "78aee2a4246d5f3df88893752bceb8fe4a315951",
"type": "github"
},
"original": {
......
<script module>
import { createHighlighterCoreSync } from 'shiki/core';
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
// Themes
import themeDarkPlus from 'shiki/themes/dark-plus.mjs';
// Languages
import html from 'shiki/langs/html.mjs';
import css from 'shiki/langs/css.mjs';
import js from 'shiki/langs/javascript.mjs';
const shiki = createHighlighterCoreSync({
engine: createJavaScriptRegexEngine(),
themes: [themeDarkPlus],
langs: [html, css, js]
});
</script>
<script lang="ts">
import type {
CodeBlockProps,
SelectionInfo,
EnhancedSelectionInfo,
SourceSelection
} from './types';
import { sourceMappingTransformer, domSelectionToSourceSelection } from './transformers';
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import type { Annotation, SourceSelection } from './types';
import CodeBlock from './CodeBlock.svelte';
let {
code = '',
lang = 'js',
theme = 'dark-plus',
// Base Style Props
base = 'overflow-hidden',
rounded = 'rounded-container',
shadow = '',
classes = '',
// Pre Style Props
preBase = '',
prePadding = '[&>pre]:p-4',
preClasses = '',
onSelectionChange = (_) => {},
showDebug = false
}: CodeBlockProps = $props();
const generatedHtml = shiki.codeToHtml(code, {
lang,
theme,
transformers: [sourceMappingTransformer],
structure: 'classic'
});
let currentSelection: SelectionInfo | null = $state(null);
code,
lang
}: {
code: string;
lang: string;
} = $props();
let showDebug = $state(true);
let sourceSelection: SourceSelection | null = $state(null);
$inspect(currentSelection);
$inspect(sourceSelection);
// Code block element reference
let codeBlockEl: HTMLElement;
function updateSelectionInfo() {
const sel = window.getSelection();
if (!sel || !isSelectionInCodeBlock(sel)) {
// currentSelection = null;
// sourceSelection = null;
return;
}
currentSelection = getSelectionInfo(sel);
sourceSelection = domSelectionToSourceSelection(sel, codeBlockEl);
if (sourceSelection) {
onSelectionChange({ ...currentSelection, source: sourceSelection });
let comment = $state('');
let scoreDelta = $state(0);
let annotationColor = $state('#ffeb3b'); // Default yellow
const handleSelectionChange = (srcSelection: SourceSelection) => {
sourceSelection = srcSelection;
};
const handleSave = () => {
if (!sourceSelection || !comment.trim()) return;
// TODO global annotation kind (ignoring from any current selection which may be active)
const date = new Date().toISOString();
if (sourceSelection.type === 'Caret') {
const newAnnotation: Annotation = {
kind: 'line',
line: sourceSelection.start.line,
scoreDelta,
comment,
createdAt: date
};
} else if (sourceSelection.type === 'Range') {
const newAnnotation: Annotation = {
kind: 'range',
selection: sourceSelection,
scoreDelta,
comment,
createdAt: date
};
}
// TODO {
// const newAnnotation: Annotation = {
// kind: 'global',
// comment,
// scoreDelta,
// createdAt: date
// }
comment = '';
scoreDelta = 0;
};
// helper to determine if the selection is a point, in which case we want to annotate the whole line it's on
function isSelectionPoint(s: SourceSelection): boolean {
return s.start.line === s.end.line && s.start.column === s.end.column;
}
function isSelectionInCodeBlock(selection: Selection): boolean {
if (!selection.rangeCount) return false;
const range = selection.getRangeAt(0);
return codeBlockEl.contains(range.commonAncestorContainer);
}
/*
function getSelectionInfo(sel: Selection): SelectionInfo {
const info: Record<string, any> = {};
......@@ -117,27 +91,39 @@
return `NODE: Type ${node.nodeType}`;
}
}
onMount(() => {
document.addEventListener('selectionchange', updateSelectionInfo);
});
onDestroy(() => {
if (!browser) return;
document.removeEventListener('selectionchange', updateSelectionInfo);
});
*/
</script>
<div
bind:this={codeBlockEl}
class="{base} {rounded} {shadow} {classes} {preBase} {prePadding} {preClasses} code-block-selectable"
>
{@html generatedHtml}
</div>
<CodeBlock {code} {lang} onSelectionChange={handleSelectionChange} />
{#if showDebug}
<div class="selection-debug">
<h3>Selection Debug</h3>
{#if sourceSelection}
<hr />
<div class="info-row">
<span class="label">Type:</span>
<span class="value">{sourceSelection.type}</span>
</div>
<div class="info-row">
<span class="label">Source Start:</span>
<span class="value"
>Line {sourceSelection.start.line}, Col {sourceSelection.start.column}</span
>
</div>
<div class="info-row">
<span class="label">Source End:</span>
<span class="value">Line {sourceSelection.end.line}, Col {sourceSelection.end.column}</span>
</div>
<div class="info-row">
<span class="label">Selected Text:</span>
<pre class="value border-2">{sourceSelection.text}</pre>
</div>
{/if}
</div>
{/if}
<!--
{#if currentSelection !== null && !currentSelection.isCollapsed}
<div class="selection-info">
<div class="info-row">
......@@ -172,84 +158,10 @@
<span class="label">Focus Offset:</span>
<span class="value">{currentSelection.focusOffset}</span>
</div>
{#if sourceSelection}
<hr />
<div class="info-row">
<span class="label">Source Start:</span>
<span class="value"
>Line {sourceSelection.start.line}, Col {sourceSelection.start.column}</span
>
</div>
<div class="info-row">
<span class="label">Source End:</span>
<span class="value"
>Line {sourceSelection.end.line}, Col {sourceSelection.end.column}</span
>
</div>
<div class="info-row">
<span class="label">Selected Text:</span>
<span class="value text-value">{sourceSelection.text}</span>
</div>
{/if}
</div>
{:else}
<p>No selection active. Click and drag to select text in the code block.</p>
{/if}
</div>
{/if}
-->
<style>
.code-block-selectable :global(pre) {
user-select: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
cursor: text;
}
.selection-debug {
margin-top: 1rem;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f5f5f5;
}
h3 {
margin-top: 0;
margin-bottom: 1rem;
}
.selection-info {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
}
.info-row {
display: grid;
grid-template-columns: 150px 1fr;
gap: 0.5rem;
}
.label {
font-weight: bold;
}
.text-value {
font-family: monospace;
word-break: break-all;
background-color: #e0e0e0;
padding: 0.25rem;
border-radius: 2px;
}
:global(.line-number) {
display: inline-block;
width: 1.5rem;
margin-right: 0.5rem;
color: #888;
text-align: right;
}
</style>
<script module>
import { createHighlighterCoreSync } from 'shiki/core';
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
// Themes
import themeDarkPlus from 'shiki/themes/dark-plus.mjs';
// Languages
import html from 'shiki/langs/html.mjs';
import css from 'shiki/langs/css.mjs';
import js from 'shiki/langs/javascript.mjs';
import python from 'shiki/langs/python.mjs';
import markdown from 'shiki/langs/markdown.mjs';
const shiki = createHighlighterCoreSync({
engine: createJavaScriptRegexEngine(),
themes: [themeDarkPlus],
langs: [html, css, js, python, markdown]
});
</script>
<script lang="ts">
import type { CodeBlockProps, SelectionInfo } from './types';
import { sourceMappingTransformer, domSelectionToSourceSelection } from './transformers';
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
let {
code = '',
lang = 'js',
theme = 'dark-plus',
// Base Style Props
base = 'overflow-hidden',
rounded = 'rounded-container',
shadow = '',
classes = '',
// Pre Style Props
preBase = '',
prePadding = '[&>pre]:p-4',
preClasses = '',
onSelectionChange = (_) => {},
showDebug = false
}: CodeBlockProps = $props();
const generatedHtml = shiki.codeToHtml(code, {
lang,
theme,
transformers: [sourceMappingTransformer],
structure: 'classic'
});
let codeBlockEl: HTMLElement;
function handleSelectionChange() {
const sel = window.getSelection();
if (!sel || !isSelectionInCodeBlock(sel)) return;
const sourceSelection = domSelectionToSourceSelection(sel, codeBlockEl);
if (sourceSelection) onSelectionChange(sourceSelection);
}
function isSelectionInCodeBlock(selection: Selection): boolean {
if (!selection.anchorNode) return false;
return codeBlockEl.contains(selection.anchorNode);
// if (!selection.rangeCount) return false;
// const range = selection.getRangeAt(0);
// return codeBlockEl.contains(range.commonAncestorContainer);
}
onMount(() => {
document.addEventListener('selectionchange', handleSelectionChange);
});
onDestroy(() => {
if (!browser) return;
document.removeEventListener('selectionchange', handleSelectionChange);
});
</script>
<div
bind:this={codeBlockEl}
class="{base} {rounded} {shadow} {classes} {preBase} {prePadding} {preClasses} code-block-selectable"
>
{@html generatedHtml}
</div>
<style>
.code-block-selectable :global(pre) {
user-select: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
cursor: crosshair;
}
.selection-debug {
margin-top: 1rem;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f5f5f5;
}
.selection-info {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
}
.info-row {
display: grid;
grid-template-columns: 150px 1fr;
gap: 0.5rem;
}
.label {
font-weight: bold;
}
.text-value {
font-family: monospace;
word-break: break-all;
background-color: #e0e0e0;
padding: 0.25rem;
border-radius: 2px;
}
:global(.line-number) {
display: inline-block;
width: 1.5rem;
margin-right: 0.5rem;
color: #888;
text-align: right;
}
</style>
export { default as AnnotationEditor } from './AnnotationEditor.svelte';
import { union, z } from 'zod';
const zeroIdx = z.number().int().gte(0);
export const sourcePosition = z.object({ line: zeroIdx, column: zeroIdx });
export type SourcePosition = z.infer<typeof sourcePosition>;
export const sourceSelection = z.object({
type: z.union([z.literal('None'), z.literal('Caret'), z.literal('Range')]),
start: sourcePosition,
end: sourcePosition,
text: z.string(),
color: z.string().optional() // TODO: validate
});
export type SourceSelection = z.infer<typeof sourceSelection>;
const annotationCommonFields = z.object({
comment: z.string(),
scoreDelta: z.number(),
createdAt: z.string().datetime()
});
export const annotation = z.discriminatedUnion('kind', [
annotationCommonFields.extend({ kind: z.literal('line'), line: z.number().gte(0) }),
annotationCommonFields.extend({ kind: z.literal('range'), selection: sourceSelection }),
annotationCommonFields.extend({ kind: z.literal('global') })
]);
export type Annotation = z.infer<typeof annotation>;
import type { ShikiTransformer } from 'shiki';
import type * as Hast from 'hast';
import type {
EnhancedSelectionInfo,
SelectionInfo,
SourcePosition,
SourceSelection
} from './types';
import type { SourcePosition, SourceSelection } from './types';
import { sourceSelection } from './schema';
export const sourceMappingTransformer: ShikiTransformer = {
name: 'source-mapping-transformer',
......@@ -42,7 +38,14 @@ export function domSelectionToSourceSelection(
const selectedText = selection.toString(); // TODO get source text, not DOM text
return { start: startPosition, end: endPosition, text: selectedText };
const result = sourceSelection.safeParse({
start: startPosition,
end: endPosition,
text: selectedText,
type: selection.type
});
return result.success ? result.data : null;
}
function findSourcePosition(
......
import type { sourceSelection, SourceSelection } from './schema';
export type { SourcePosition, SourceSelection, Annotation } from './schema.ts';
/**
* Props for the CodeBlock component
*/
......@@ -15,7 +19,7 @@ export interface CodeBlockProps {
prePadding?: string;
preClasses?: string;
// Selection props
onSelectionChange?: (selInfo: EnhancedSelectionInfo) => void;
onSelectionChange?: (sourceSelection: SourceSelection) => void;
// Debug view
showDebug?: boolean;
}
......@@ -28,26 +32,9 @@ export type SelectionInfo = {
/**
* Enhanced SelectionInfo that includes source positions
*/
export interface EnhancedSelectionInfo extends SelectionInfo {
source?: SourceSelection;
}
/**
* Source code position (0-based)
*/
export interface SourcePosition {
line: number; // 0-based line number
column: number; // 0-based column number
}
/**
* Selection range in source code
*/
export interface SourceSelection {
start: SourcePosition;
end: SourcePosition;
text: string;
}
// export interface EnhancedSelectionInfo extends SelectionInfo {
// source?: SourceSelection;
// }
/**
* Information about a DOM Selection
......
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { SelectionInfo, Annotation } from './types';
// Props
let {
selection = null
}: {
selection: SelectionInfo | null;
} = $props();
// Local state
let commentText = $state('');
let annotationColor = $state('#ffeb3b'); // Default yellow
// Event dispatcher
const dispatch = createEventDispatcher<{
save: Annotation;
}>();
// Handle save button click
function handleSave() {
if (!selection || selection.isCollapsed || !commentText.trim()) return;
const newAnnotation: Annotation = {
id: crypto.randomUUID(),
selection,
content: commentText.trim(),
color: annotationColor,
createdAt: new Date()
};
dispatch('save', newAnnotation);
// Reset the form
commentText = '';
}
</script>
<div class="annotation-editor">
<h3>Create Annotation</h3>
{#if selection && !selection.isCollapsed}
<div class="selected-text">
<strong>Selected Text:</strong>
<span class="text-snippet">{selection.text}</span>
</div>
<div class="position-info">
<strong>Position:</strong>
<span class="position-details">
Lines {selection.position?.start.line}-{selection.position?.end.line}, Columns {selection
.position?.start.column}-{selection.position?.end.column}
</span>
</div>
<div class="form-group">
<label for="comment">Comment:</label>
<textarea
id="comment"
bind:value={commentText}
placeholder="Add your annotation comment here..."
rows="3"
></textarea>
</div>
<div class="form-group">
<label for="color">Highlight Color:</label>
<input type="color" id="color" bind:value={annotationColor} />
</div>
<button class="save-btn" on:click={handleSave} disabled={!commentText.trim()}>
Save Annotation
</button>
{:else}
<p>Select text in the code to add an annotation</p>
{/if}
</div>
<style>
.annotation-editor {
margin-top: 1rem;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f5f5f5;
}
.selected-text {
margin-bottom: 1rem;
padding: 0.5rem;
background-color: #e0e0e0;
border-radius: 4px;
}
.text-snippet {
font-family: monospace;
word-break: break-all;
}
.position-info {
margin-bottom: 1rem;
padding: 0.5rem;
background-color: #e0e0e0;
border-radius: 4px;
font-size: 0.9rem;
}
.position-details {
font-family: monospace;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.save-btn {
padding: 0.5rem 1rem;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.save-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.save-btn:not(:disabled):hover {
background-color: #45a049;
}
</style>
<script lang="ts">
import CodeBlock from '$lib/components/CodeBlock/CodeBlock.svelte';
import type { EnhancedSelectionInfo } from '$lib/components/CodeBlock/types';
import CodeBlock from '$lib/components/AnnotationEditor/CodeBlock.svelte';
import type { SourceSelection } from '$lib/components/AnnotationEditor/types';
const sampleCode = `function calculateTotal(items) {
return items
......@@ -18,9 +18,9 @@ const total = calculateTotal(cart);
console.log(\`Total: \$\${total.toFixed(2)}\`);`;
let showDebug = $state(true);
let selectionInfo: EnhancedSelectionInfo | null = $state(null);
let selectionInfo: SourceSelection | null = $state(null);
function handleSelectionChange(info: EnhancedSelectionInfo) {
function handleSelectionChange(info: SourceSelection) {
selectionInfo = info;
}
......@@ -48,19 +48,19 @@ console.log(\`Total: \$\${total.toFixed(2)}\`);`;
<div class="debug-panel">
<h3>Enhanced Selection Info</h3>
{#if selectionInfo?.source}
{#if selectionInfo}
<div class="source-info">
<div class="info-row">
<span class="label">Selection Start:</span>
<span class="value">{formatPosition(selectionInfo.source.start)}</span>
<span class="value">{formatPosition(selectionInfo.start)}</span>
</div>
<div class="info-row">
<span class="label">Selection End:</span>
<span class="value">{formatPosition(selectionInfo.source.end)}</span>
<span class="value">{formatPosition(selectionInfo.end)}</span>
</div>
<div class="info-row">
<span class="label">Selected Text:</span>
<pre class="selected-text">{selectionInfo.source.text}</pre>
<pre class="selected-text">{selectionInfo.text}</pre>
</div>
</div>
{:else}
......
<script lang="ts">
import { enhance } from '$app/forms';
import Editor from '$lib/components/Editor/Editor.svelte';
import CodeBlock from '../../../../../lib/components/CodeBlock/CodeBlock.svelte';
import AnnotationEditor from '$lib/components/AnnotationEditor/AnnotationEditor.svelte';
('$lib/components/AnnotationEditor');
import type { PageData } from './$types';
const { data } = $props<{ data: PageData }>();
......@@ -128,10 +128,10 @@
</div>
<div class="rounded-md border bg-gray-50 p-4">
<h2 class="mb-2 text-xl font-semibold">Submission Code</h2>
<h2 class="mb-2 text-xl font-semibold">Submission Editor</h2>
<!--<pre class="h-64 overflow-auto rounded-md bg-gray-100 p-3 text-sm">{submissionCode}</pre>-->
<!-- <CodeBlock code={submissionCode} /> -->
<Editor code={submissionCode} lang={'python'} />
<AnnotationEditor code={submissionCode} lang={'python'} />
</div>
<details class="rounded-md border bg-gray-50 p-4">
......