Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"prefersReducedMotion": true,
"spinnerTipsEnabled": false
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ src/main/frontend/node_modules/
src/main/frontend/node/
src/main/frontend/dist/
src/main/frontend/tsconfig.tsbuildinfo
# Generated explorer CSS (rebuild via: cd src/main/frontend && npm run build:explorer-css)
src/main/resources/static/css/explorer.css

# Distribution
*.tar.gz
Expand Down
57 changes: 57 additions & 0 deletions src/main/frontend/explorer-tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Config } from 'tailwindcss';

/**
* Tailwind config for the Thymeleaf explorer UI templates.
* Separate from the React app config to preserve the original blue brand colors.
*/
export default {
darkMode: 'class',
content: [
'../resources/templates/**/*.html',
],
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a5f',
},
surface: {
DEFAULT: '#f8fafc',
dark: '#0f172a',
},
card: {
DEFAULT: '#ffffff',
dark: '#1e293b',
},
muted: {
DEFAULT: '#64748b',
dark: '#94a3b8',
},
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(8px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
},
},
plugins: [],
} satisfies Config;
3 changes: 2 additions & 1 deletion src/main/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build": "tsc -b && vite build && npm run build:explorer-css",
"build:explorer-css": "npx tailwindcss -c ./explorer-tailwind.config.ts -i ./src/explorer.css -o ../resources/static/css/explorer.css --minify",
"preview": "vite preview"
},
"dependencies": {
Expand Down
19 changes: 19 additions & 0 deletions src/main/frontend/src/explorer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

[x-cloak] { display: none !important; }
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.htmx-request.htmx-indicator { display: inline-block; }
.card-stagger:nth-child(1) { animation-delay: 0ms; }
.card-stagger:nth-child(2) { animation-delay: 30ms; }
.card-stagger:nth-child(3) { animation-delay: 60ms; }
.card-stagger:nth-child(4) { animation-delay: 90ms; }
.card-stagger:nth-child(5) { animation-delay: 120ms; }
.card-stagger:nth-child(6) { animation-delay: 150ms; }
.card-stagger:nth-child(7) { animation-delay: 180ms; }
.card-stagger:nth-child(8) { animation-delay: 210ms; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ public String nodesByKind(
return "explorer/nodes";
}

@GetMapping("/node/{nodeId}")
public String nodeDetail(@PathVariable String nodeId, Model model) {
@GetMapping("/node")
public String nodeDetail(@RequestParam String nodeId, Model model) {
Map<String, Object> detail = queryService.nodeDetailWithEdges(nodeId);
model.addAttribute("detail", detail);
return "explorer/detail";
Expand All @@ -76,8 +76,8 @@ public String nodesFragment(
return "explorer/fragments/nodes-grid";
}

@GetMapping("/fragments/detail/{nodeId}")
public String detailFragment(@PathVariable String nodeId, Model model) {
@GetMapping("/fragments/detail")
public String detailFragment(@RequestParam String nodeId, Model model) {
Map<String, Object> detail = queryService.nodeDetailWithEdges(nodeId);
model.addAttribute("detail", detail);
return "explorer/fragments/detail-panel";
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/static/css/explorer.css

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ <h2 class="font-semibold mb-4">
<span th:if="${edge['target_kind'] != null}"
class="text-[10px] px-1.5 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 capitalize whitespace-nowrap"
th:text="${edge['target_kind']}">class</span>
<a th:href="@{/ui/node/{id}(id=${edge['target']})}"
<a th:href="@{/ui/node(nodeId=${edge['target']})}"
class="text-brand-600 dark:text-brand-400 hover:underline text-sm truncate"
th:text="${edge['target_label'] != null ? edge['target_label'] : edge['target']}">target</a>
</div>
Expand Down Expand Up @@ -137,7 +137,7 @@ <h2 class="font-semibold mb-4">
th:text="${node['kind']}">class</span>
<svg class="w-4 h-4 text-muted dark:text-muted-dark shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
<div class="flex items-center gap-2 min-w-0">
<a th:href="@{/ui/node/{id}(id=${node['id']})}"
<a th:href="@{/ui/node(nodeId=${node['id']})}"
class="text-brand-600 dark:text-brand-400 hover:underline text-sm truncate"
th:text="${node['label']}">label</a>
<span th:if="${node['layer'] != null}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h2 class="text-lg font-semibold capitalize" th:text="${kind}">kind</h2>
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<a th:href="@{/ui/node/{id}(id=${node['id']})}"
<a th:href="@{/ui/node(nodeId=${node['id']})}"
class="font-medium text-brand-600 dark:text-brand-400 hover:underline truncate"
th:text="${node['label']}">label</a>
<span th:if="${node['layer'] != null}"
Expand All @@ -49,7 +49,7 @@ <h2 class="text-lg font-semibold capitalize" th:text="${kind}">kind</h2>
class="text-[10px] px-1.5 py-0.5 rounded bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 font-mono"
th:text="${ann}">@ann</span>
</div>
<button th:attr="hx-get='/ui/fragments/detail/' + ${node['id']}"
<button th:attr="hx-get='/ui/fragments/detail?nodeId=' + ${#uris.escapeQueryStringParam(node['id'])}"
hx-target="#content" hx-swap="innerHTML"
class="text-xs font-medium px-3 py-1.5 rounded-md text-muted dark:text-muted-dark hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">
Details
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ <h2 class="text-lg font-semibold">
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<a th:href="@{/ui/node/{id}(id=${node['id']})}"
<a th:href="@{/ui/node(nodeId=${node['id']})}"
class="font-medium text-brand-600 dark:text-brand-400 hover:underline truncate"
th:text="${node['label']}">label</a>
<span class="text-[10px] px-1.5 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 capitalize"
Expand All @@ -38,7 +38,7 @@ <h2 class="text-lg font-semibold">
<div th:if="${node['file_path'] != null}" class="text-xs text-muted dark:text-muted-dark font-mono truncate"
th:text="${node['file_path']}">file</div>
</div>
<button th:attr="hx-get='/ui/fragments/detail/' + ${node['id']}"
<button th:attr="hx-get='/ui/fragments/detail?nodeId=' + ${#uris.escapeQueryStringParam(node['id'])}"
hx-target="#content" hx-swap="innerHTML"
class="text-xs font-medium px-3 py-1.5 rounded-md text-muted dark:text-muted-dark hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors whitespace-nowrap">
Details
Expand Down
56 changes: 8 additions & 48 deletions src/main/resources/templates/explorer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OSSCodeIQ Explorer</title>

<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
brand: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a5f' },
surface: { DEFAULT: '#f8fafc', dark: '#0f172a' },
card: { DEFAULT: '#ffffff', dark: '#1e293b' },
muted: { DEFAULT: '#64748b', dark: '#94a3b8' }
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
},
keyframes: {
fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } },
slideUp: { '0%': { opacity: '0', transform: 'translateY(8px)' }, '100%': { opacity: '1', transform: 'translateY(0)' } },
}
}
}
}
</script>
<link rel="stylesheet" th:href="@{/css/explorer.css}">
<script th:src="@{/js/htmx.min.js}"></script>
<script defer th:src="@{/js/alpine.min.js}"></script>

<style>
[x-cloak] { display: none !important; }
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.htmx-request.htmx-indicator { display: inline-block; }
.card-stagger:nth-child(1) { animation-delay: 0ms; }
.card-stagger:nth-child(2) { animation-delay: 30ms; }
.card-stagger:nth-child(3) { animation-delay: 60ms; }
.card-stagger:nth-child(4) { animation-delay: 90ms; }
.card-stagger:nth-child(5) { animation-delay: 120ms; }
.card-stagger:nth-child(6) { animation-delay: 150ms; }
.card-stagger:nth-child(7) { animation-delay: 180ms; }
.card-stagger:nth-child(8) { animation-delay: 210ms; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
</style>

<script>
function themeManager() {
return {
Expand Down Expand Up @@ -349,7 +308,7 @@ <h3 class="font-semibold text-sm capitalize" th:text="${kindEntry['kind']}">kind
Explore
</a>
<button hx-get="/ui/fragments/nodes/{kind}"
th:attr="hx-get='/ui/fragments/nodes/' + ${kindEntry['kind']}"
th:attr="hx-get='/ui/fragments/nodes/' + ${#uris.escapePathSegment(kindEntry['kind'])}"
hx-target="#content"
hx-swap="innerHTML"
class="text-xs font-medium px-3 py-1.5 rounded-md text-muted dark:text-muted-dark hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
Expand All @@ -366,11 +325,12 @@ <h3 class="font-semibold text-sm capitalize" th:text="${kindEntry['kind']}">kind
<svg class="mx-auto w-12 h-12 text-muted dark:text-muted-dark mb-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"/>
</svg>
<p class="text-muted dark:text-muted-dark">No nodes found. Run an analysis first.</p>
<button hx-post="/api/analyze" hx-swap="none"
class="mt-4 text-sm font-medium px-4 py-2 rounded-lg bg-brand-600 text-white hover:bg-brand-700 transition-colors">
Run Analysis
</button>
<p class="text-muted dark:text-muted-dark mb-2">No nodes found. Run the analysis pipeline from the CLI first:</p>
<div class="inline-block text-left bg-slate-100 dark:bg-slate-800 rounded-lg p-4 mt-2">
<code class="block text-sm font-mono text-slate-700 dark:text-slate-300 mb-1">code-iq index /path/to/repo</code>
<code class="block text-sm font-mono text-slate-700 dark:text-slate-300 mb-1">code-iq enrich /path/to/repo</code>
<code class="block text-sm font-mono text-slate-700 dark:text-slate-300">code-iq serve /path/to/repo</code>
</div>
</div>
</div>
</main>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ void nodeDetailShouldReturnDetailView() throws Exception {

when(queryService.nodeDetailWithEdges("cls:test:class:UserService")).thenReturn(detail);

mockMvc.perform(get("/ui/node/cls:test:class:UserService"))
mockMvc.perform(get("/ui/node").param("nodeId", "cls:test:class:UserService"))
.andExpect(status().isOk())
.andExpect(view().name("explorer/detail"))
.andExpect(model().attributeExists("detail"));
Expand All @@ -135,7 +135,7 @@ void nodeDetailShouldReturnDetailView() throws Exception {
void nodeDetailWithNullShouldStillReturnView() throws Exception {
when(queryService.nodeDetailWithEdges("missing")).thenReturn(null);

mockMvc.perform(get("/ui/node/missing"))
mockMvc.perform(get("/ui/node").param("nodeId", "missing"))
.andExpect(status().isOk())
.andExpect(view().name("explorer/detail"));
}
Expand Down Expand Up @@ -201,7 +201,7 @@ void detailFragmentShouldReturnFragmentView() throws Exception {

when(queryService.nodeDetailWithEdges("n1")).thenReturn(detail);

mockMvc.perform(get("/ui/fragments/detail/n1"))
mockMvc.perform(get("/ui/fragments/detail").param("nodeId", "n1"))
.andExpect(status().isOk())
.andExpect(view().name("explorer/fragments/detail-panel"))
.andExpect(model().attributeExists("detail"));
Expand Down
Loading