[OCaml] Mobile-friendly clone of cgit.
feat add inline directory expand/collapse in files view
Directories in the files view now use <details>/<summary> to expand and collapse inline without JavaScript or page reloads. The Tree.preload resolver recursively loads subdirectory contents for directories with fewer than 10 entries. Directories with 10+ entries show a teaser message ('Directory contains more than 10 items...') that links to the full subdirectory page. A right-aligned chevron (›) rotates on open. Nested entries are indented with a left border for visual hierarchy. Directory names remain clickable links to their dedicated tree page for deep-linking.
Changed files
lib/handlers.ml
@@ -141,13 +141,16 @@
141
141
142
142
let files_at_head repository context =
143
143
let* tree = Resolvers.Tree.head repository in
144
Removed:
Views.Repo.files context [] tree
144
Added:
let* preloaded = Resolvers.Tree.preload repository tree in
145
Added:
Views.Repo.files context [] preloaded
145
146
146
147
let file_id repository context id =
147
148
let* trail = Resolvers.Tree.find_path repository id in
148
149
let* object_ = Resolvers.blob_or_tree repository id in
149
150
match object_ with
150
Removed:
| `Tree tree -> Views.Repo.files context trail tree
151
Added:
| `Tree tree ->
152
Added:
let* preloaded = Resolvers.Tree.preload repository tree in
153
Added:
Views.Repo.files context trail preloaded
151
154
| `Blob blob -> Views.Repo.file context trail blob
152
155
153
156
let branches repository context =
lib/resolvers.ml
@@ -331,6 +331,11 @@
331
331
module Tree = struct
332
332
type t = { entries : Entry.t list }
333
333
334
Added:
type preloaded_entry = {
335
Added:
entry : Entry.t;
336
Added:
children : preloaded_entry list option;
337
Added:
}
338
Added:
334
339
let to_t tree =
335
340
{ entries = Store.Value.Tree.to_list tree |> List.map Entry.to_t }
336
341
@@ -351,6 +356,33 @@
351
356
let head repository =
352
357
let* hash = head_tree_hash repository in
353
358
of_hash repository hash
359
Added:
360
Added:
let preload repository tree =
361
Added:
let rec preload_entry (entry : Entry.t) =
362
Added:
if entry.perm <> Entry.Dir then
363
Added:
Lwt_result.return { entry; children = None }
364
Added:
else
365
Added:
let* hash = hash_of_hex entry.hash in
366
Added:
let* subtree =
367
Added:
Lwt_result.bind (read_value repository hash) @@ function
368
Added:
| Git.Value.Tree t -> Lwt_result.return (to_t t)
369
Added:
| _ -> Lwt_result.return { entries = [] }
370
Added:
in
371
Added:
let count = List.length subtree.entries in
372
Added:
if count >= 10 then Lwt_result.return { entry; children = Some [] }
373
Added:
else
374
Added:
let* children = preload_entries subtree.entries in
375
Added:
Lwt_result.return { entry; children = Some children }
376
Added:
and preload_entries entries =
377
Added:
let rec go acc = function
378
Added:
| [] -> Lwt_result.return (List.rev acc)
379
Added:
| e :: rest ->
380
Added:
let* pe = preload_entry e in
381
Added:
go (pe :: acc) rest
382
Added:
in
383
Added:
go [] entries
384
Added:
in
385
Added:
preload_entries tree.entries
354
386
355
387
let find_path repository target_hash =
356
388
let* target = hash_of_hex target_hash in
lib/resolvers.mli
@@ -80,7 +80,13 @@
80
80
module Tree : sig
81
81
type t = { entries : Entry.t list }
82
82
83
Added:
type preloaded_entry = {
84
Added:
entry : Entry.t;
85
Added:
children : preloaded_entry list option;
86
Added:
}
87
Added:
83
88
val head : repository -> (t, error) Lwt_result.t
89
Added:
val preload : repository -> t -> (preloaded_entry list, error) Lwt_result.t
84
90
85
91
val find_path :
86
92
repository -> string -> ((string * string) list, error) Lwt_result.t
lib/static/styles.css
@@ -559,6 +559,83 @@
559
559
color: white;
560
560
}
561
561
562
Added:
/* Tree directory expand/collapse */
563
Added:
564
Added:
.tree-dir {
565
Added:
padding: 0;
566
Added:
}
567
Added:
568
Added:
.tree-dir > details {
569
Added:
width: 100%;
570
Added:
}
571
Added:
572
Added:
.tree-toggle {
573
Added:
display: flex;
574
Added:
align-items: center;
575
Added:
justify-content: space-between;
576
Added:
cursor: pointer;
577
Added:
list-style: none;
578
Added:
padding: 0.75em;
579
Added:
min-height: 44px;
580
Added:
box-sizing: border-box;
581
Added:
}
582
Added:
583
Added:
.tree-toggle::-webkit-details-marker {
584
Added:
display: none;
585
Added:
}
586
Added:
587
Added:
.tree-toggle::marker {
588
Added:
content: "";
589
Added:
}
590
Added:
591
Added:
.tree-toggle::after {
592
Added:
content: "\203a";
593
Added:
font-size: 1.3em;
594
Added:
color: #b0b0b0;
595
Added:
transition: transform 0.15s ease;
596
Added:
flex-shrink: 0;
597
Added:
min-width: 44px;
598
Added:
text-align: center;
599
Added:
}
600
Added:
601
Added:
details[open] > .tree-toggle::after {
602
Added:
transform: rotate(90deg);
603
Added:
}
604
Added:
605
Added:
.tree-toggle a {
606
Added:
color: white;
607
Added:
text-decoration: none;
608
Added:
}
609
Added:
610
Added:
.tree-toggle a:hover {
611
Added:
text-decoration: underline;
612
Added:
}
613
Added:
614
Added:
.tree-nested {
615
Added:
padding-left: 1.5em;
616
Added:
list-style: none;
617
Added:
margin: 0;
618
Added:
}
619
Added:
620
Added:
.tree-nested li {
621
Added:
border-left: 1px solid #3a3a3a;
622
Added:
}
623
Added:
624
Added:
.tree-overflow {
625
Added:
padding: 0.5em 0.75em;
626
Added:
}
627
Added:
628
Added:
.tree-overflow a {
629
Added:
color: #b0b0b0;
630
Added:
text-decoration: none;
631
Added:
font-style: italic;
632
Added:
}
633
Added:
634
Added:
.tree-overflow a:hover {
635
Added:
color: white;
636
Added:
text-decoration: underline;
637
Added:
}
638
Added:
562
639
.diff-file {
563
640
margin: 1.5em 0;
564
641
border: 1px solid #3a3a3a;
lib/views/repo.ml
@@ -152,6 +152,54 @@
152
152
in
153
153
HTML.(li [] [ Routes.link_to route text ])
154
154
155
Added:
let rec li_of_preloaded repo (pe : Resolvers.Tree.preloaded_entry) =
156
Added:
let entry = pe.entry in
157
Added:
let route = Routes.File (repo, entry.hash) in
158
Added:
match pe.children with
159
Added:
| None ->
160
Added:
(* Regular file *)
161
Added:
HTML.(li [] [ Routes.link_to route (txt "%s" entry.name) ])
162
Added:
| Some [] ->
163
Added:
(* Directory with 10+ items — show teaser *)
164
Added:
HTML.(
165
Added:
li
166
Added:
[ class_ "tree-dir" ]
167
Added:
[
168
Added:
details []
169
Added:
[
170
Added:
summary
171
Added:
[ class_ "tree-toggle" ]
172
Added:
[ Routes.link_to route (txt "%s/" entry.name) ];
173
Added:
ul
174
Added:
[ class_ "tree-nested" ]
175
Added:
[
176
Added:
li
177
Added:
[ class_ "tree-overflow" ]
178
Added:
[
179
Added:
Routes.link_to route
180
Added:
(txt
181
Added:
"Directory contains more than 10 items\xe2\x80\xa6");
182
Added:
];
183
Added:
];
184
Added:
];
185
Added:
])
186
Added:
| Some children ->
187
Added:
(* Directory with <10 items — fully preloaded *)
188
Added:
HTML.(
189
Added:
li
190
Added:
[ class_ "tree-dir" ]
191
Added:
[
192
Added:
details []
193
Added:
[
194
Added:
summary
195
Added:
[ class_ "tree-toggle" ]
196
Added:
[ Routes.link_to route (txt "%s/" entry.name) ];
197
Added:
ul
198
Added:
[ class_ "tree-nested" ]
199
Added:
(List.map (li_of_preloaded repo) children);
200
Added:
];
201
Added:
])
202
Added:
155
203
let summary context branches commits =
156
204
render_page context ~active:Summary
157
205
HTML.
@@ -268,11 +316,11 @@
268
316
[ class_ "breadcrumbs"; Aria.label "File path" ]
269
317
[ ul [] (root_link :: crumbs) ])
270
318
271
Removed:
let files context trail (tree : Resolvers.Tree.t) =
319
Added:
let files context trail (entries : Resolvers.Tree.preloaded_entry list) =
272
320
let bc = breadcrumbs context.repo trail in
273
321
let tb = toolbar [ bc ] in
274
322
render_page context ~active:Files
275
Removed:
HTML.[ tb; ul [] (List.map (li_of_entry context.repo) tree.entries) ]
323
Added:
HTML.[ tb; ul [] (List.map (li_of_preloaded context.repo) entries) ]
276
324
277
325
let file ?(active = Layout.Files) context trail (blob : Resolvers.Blob.t) =
278
326
let to_numbered_line number line =