Skip to content

Commit 9a310da

Browse files
aksOpsPaperclip-Paperclipclaude
committed
fix: address code review — query params for node IDs, correct brand colors
- F2: Switch /ui/node and /ui/fragments/detail from path variables to query parameters (?nodeId=...) to avoid Tomcat %2F rejection for node IDs containing slashes. Use #uris.escapeQueryStringParam() in templates. - F2: Update all th:href links in detail-panel, nodes-grid, and search-results to use query param format. - F2: Update ExplorerController endpoints and tests accordingly. - F3: Create separate explorer-tailwind.config.ts with original blue brand colors (#3b82f6/#2563eb) and 0.3s animation timing, avoiding regression in the React app's indigo branding. - F3: Revert React tailwind.config.ts to original (no template scanning). - F3: Add explorer.css to .gitignore as a build artifact. Co-Authored-By: Paperclip <noreply@paperclip.ing> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d28f062 commit 9a310da

9 files changed

Lines changed: 74 additions & 29 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ src/main/frontend/node_modules/
4343
src/main/frontend/node/
4444
src/main/frontend/dist/
4545
src/main/frontend/tsconfig.tsbuildinfo
46+
# Generated explorer CSS (rebuild via: cd src/main/frontend && npm run build:explorer-css)
47+
src/main/resources/static/css/explorer.css
4648

4749
# Distribution
4850
*.tar.gz
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Config } from 'tailwindcss';
2+
3+
/**
4+
* Tailwind config for the Thymeleaf explorer UI templates.
5+
* Separate from the React app config to preserve the original blue brand colors.
6+
*/
7+
export default {
8+
darkMode: 'class',
9+
content: [
10+
'../resources/templates/**/*.html',
11+
],
12+
theme: {
13+
extend: {
14+
colors: {
15+
brand: {
16+
50: '#eff6ff',
17+
100: '#dbeafe',
18+
200: '#bfdbfe',
19+
300: '#93c5fd',
20+
400: '#60a5fa',
21+
500: '#3b82f6',
22+
600: '#2563eb',
23+
700: '#1d4ed8',
24+
800: '#1e40af',
25+
900: '#1e3a5f',
26+
},
27+
surface: {
28+
DEFAULT: '#f8fafc',
29+
dark: '#0f172a',
30+
},
31+
card: {
32+
DEFAULT: '#ffffff',
33+
dark: '#1e293b',
34+
},
35+
muted: {
36+
DEFAULT: '#64748b',
37+
dark: '#94a3b8',
38+
},
39+
},
40+
animation: {
41+
'fade-in': 'fadeIn 0.3s ease-out',
42+
'slide-up': 'slideUp 0.3s ease-out',
43+
},
44+
keyframes: {
45+
fadeIn: {
46+
'0%': { opacity: '0' },
47+
'100%': { opacity: '1' },
48+
},
49+
slideUp: {
50+
'0%': { opacity: '0', transform: 'translateY(8px)' },
51+
'100%': { opacity: '1', transform: 'translateY(0)' },
52+
},
53+
},
54+
},
55+
},
56+
plugins: [],
57+
} satisfies Config;

src/main/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"dev": "vite",
88
"build": "tsc -b && vite build && npm run build:explorer-css",
9-
"build:explorer-css": "npx tailwindcss -i ./src/explorer.css -o ../resources/static/css/explorer.css --minify",
9+
"build:explorer-css": "npx tailwindcss -c ./explorer-tailwind.config.ts -i ./src/explorer.css -o ../resources/static/css/explorer.css --minify",
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {

src/main/frontend/tailwind.config.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ import type { Config } from 'tailwindcss';
22

33
export default {
44
darkMode: 'class',
5-
content: [
6-
'./index.html',
7-
'./src/**/*.{js,ts,jsx,tsx}',
8-
'../resources/templates/**/*.html',
9-
],
5+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
106
theme: {
117
extend: {
128
colors: {
@@ -24,7 +20,6 @@ export default {
2420
950: '#1e1b4b',
2521
},
2622
surface: {
27-
DEFAULT: '#f8fafc',
2823
0: '#ffffff',
2924
50: '#f8fafc',
3025
100: '#f1f5f9',
@@ -38,15 +33,6 @@ export default {
3833
850: '#162032',
3934
900: '#0f172a',
4035
950: '#0a0a1a',
41-
dark: '#0f172a',
42-
},
43-
card: {
44-
DEFAULT: '#ffffff',
45-
dark: '#1e293b',
46-
},
47-
muted: {
48-
DEFAULT: '#64748b',
49-
dark: '#94a3b8',
5036
},
5137
},
5238
fontFamily: {

src/main/java/io/github/randomcodespace/iq/web/ExplorerController.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ public String nodesByKind(
5050
return "explorer/nodes";
5151
}
5252

53-
@GetMapping("/node/{nodeId}")
54-
public String nodeDetail(@PathVariable String nodeId, Model model) {
53+
@GetMapping("/node")
54+
public String nodeDetail(@RequestParam String nodeId, Model model) {
5555
Map<String, Object> detail = queryService.nodeDetailWithEdges(nodeId);
5656
model.addAttribute("detail", detail);
5757
return "explorer/detail";
@@ -76,8 +76,8 @@ public String nodesFragment(
7676
return "explorer/fragments/nodes-grid";
7777
}
7878

79-
@GetMapping("/fragments/detail/{nodeId}")
80-
public String detailFragment(@PathVariable String nodeId, Model model) {
79+
@GetMapping("/fragments/detail")
80+
public String detailFragment(@RequestParam String nodeId, Model model) {
8181
Map<String, Object> detail = queryService.nodeDetailWithEdges(nodeId);
8282
model.addAttribute("detail", detail);
8383
return "explorer/fragments/detail-panel";

src/main/resources/templates/explorer/fragments/detail-panel.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ <h2 class="font-semibold mb-4">
107107
<span th:if="${edge['target_kind'] != null}"
108108
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"
109109
th:text="${edge['target_kind']}">class</span>
110-
<a th:href="@{/ui/node/{id}(id=${edge['target']})}"
110+
<a th:href="@{/ui/node(nodeId=${edge['target']})}"
111111
class="text-brand-600 dark:text-brand-400 hover:underline text-sm truncate"
112112
th:text="${edge['target_label'] != null ? edge['target_label'] : edge['target']}">target</a>
113113
</div>
@@ -137,7 +137,7 @@ <h2 class="font-semibold mb-4">
137137
th:text="${node['kind']}">class</span>
138138
<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>
139139
<div class="flex items-center gap-2 min-w-0">
140-
<a th:href="@{/ui/node/{id}(id=${node['id']})}"
140+
<a th:href="@{/ui/node(nodeId=${node['id']})}"
141141
class="text-brand-600 dark:text-brand-400 hover:underline text-sm truncate"
142142
th:text="${node['label']}">label</a>
143143
<span th:if="${node['layer'] != null}"

src/main/resources/templates/explorer/fragments/nodes-grid.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ <h2 class="text-lg font-semibold capitalize" th:text="${kind}">kind</h2>
2525
<div class="flex items-start justify-between gap-4">
2626
<div class="flex-1 min-w-0">
2727
<div class="flex items-center gap-2 mb-1">
28-
<a th:href="@{/ui/node/{id}(id=${node['id']})}"
28+
<a th:href="@{/ui/node(nodeId=${node['id']})}"
2929
class="font-medium text-brand-600 dark:text-brand-400 hover:underline truncate"
3030
th:text="${node['label']}">label</a>
3131
<span th:if="${node['layer'] != null}"
@@ -49,7 +49,7 @@ <h2 class="text-lg font-semibold capitalize" th:text="${kind}">kind</h2>
4949
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"
5050
th:text="${ann}">@ann</span>
5151
</div>
52-
<button th:attr="hx-get='/ui/fragments/detail/' + ${#uris.escapePathSegment(node['id'])}"
52+
<button th:attr="hx-get='/ui/fragments/detail?nodeId=' + ${#uris.escapeQueryStringParam(node['id'])}"
5353
hx-target="#content" hx-swap="innerHTML"
5454
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">
5555
Details

src/main/resources/templates/explorer/fragments/search-results.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ <h2 class="text-lg font-semibold">
2626
<div class="flex items-start justify-between gap-4">
2727
<div class="flex-1 min-w-0">
2828
<div class="flex items-center gap-2 mb-1">
29-
<a th:href="@{/ui/node/{id}(id=${node['id']})}"
29+
<a th:href="@{/ui/node(nodeId=${node['id']})}"
3030
class="font-medium text-brand-600 dark:text-brand-400 hover:underline truncate"
3131
th:text="${node['label']}">label</a>
3232
<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"
@@ -38,7 +38,7 @@ <h2 class="text-lg font-semibold">
3838
<div th:if="${node['file_path'] != null}" class="text-xs text-muted dark:text-muted-dark font-mono truncate"
3939
th:text="${node['file_path']}">file</div>
4040
</div>
41-
<button th:attr="hx-get='/ui/fragments/detail/' + ${#uris.escapePathSegment(node['id'])}"
41+
<button th:attr="hx-get='/ui/fragments/detail?nodeId=' + ${#uris.escapeQueryStringParam(node['id'])}"
4242
hx-target="#content" hx-swap="innerHTML"
4343
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">
4444
Details

src/test/java/io/github/randomcodespace/iq/web/ExplorerControllerTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ void nodeDetailShouldReturnDetailView() throws Exception {
125125

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

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

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

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

204-
mockMvc.perform(get("/ui/fragments/detail/n1"))
204+
mockMvc.perform(get("/ui/fragments/detail").param("nodeId", "n1"))
205205
.andExpect(status().isOk())
206206
.andExpect(view().name("explorer/fragments/detail-panel"))
207207
.andExpect(model().attributeExists("detail"));

0 commit comments

Comments
 (0)