Skip to content

Commit 3ab8cdb

Browse files
authored
Merge pull request #2 from ThinkR-open/quick-wins
Quick wins
2 parents 78d0c8e + 5372aa0 commit 3ab8cdb

7 files changed

Lines changed: 432 additions & 6 deletions

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: css2r
22
Title: An Amazing Shiny App
3-
Version: 0.1.0
3+
Version: 0.2.0
44
Authors@R:
55
person("Arthur", "Bréant", , "arthur@thinkr.fr", role = c("aut", "cre"))
66
Description: What the package does (one paragraph).

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# css2r 0.2.0
2+
13
# css2r 0.1.0
24

35
* Update UI with new features

R/app_server.R

Lines changed: 212 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#' @import shiny
66
#' @noRd
77
app_server <- function(input, output, session) {
8-
rv <- reactiveValues(site = NULL)
8+
rv <- reactiveValues(site = NULL, preview_primary = NULL, preview_secondary = NULL)
99

1010
observeEvent(input$analyze_btn, {
1111
rv$result <- NULL
@@ -66,6 +66,12 @@ app_server <- function(input, output, session) {
6666
observeEvent(rv$continue, {
6767
req(rv$site)
6868

69+
session$setCurrentTheme(rv$site$shiny_theme)
70+
71+
top_colors_df_init <- rv$site$top_colors$top_colors
72+
rv$preview_primary <- top_colors_df_init$Color[1]
73+
rv$preview_secondary <- if (nrow(top_colors_df_init) > 1) top_colors_df_init$Color[2] else top_colors_df_init$Color[1]
74+
6975
session$sendCustomMessage(
7076
type = "apply_gradient",
7177
message = list(
@@ -107,7 +113,22 @@ app_server <- function(input, output, session) {
107113
tags$p(
108114
class = "color-section-total",
109115
paste0(nrow(rv$site$all_colors), " unique colors in total")
110-
)
116+
),
117+
if (!is.null(rv$site$top_colors$white_black) && nrow(rv$site$top_colors$white_black) > 0) {
118+
wb <- rv$site$top_colors$white_black
119+
div(
120+
class = "color-section-neutrals",
121+
tags$span(class = "color-section-neutrals-label", "Neutrals"),
122+
lapply(seq_len(nrow(wb)), function(j) {
123+
hex_wb <- wb$Color[j]
124+
div(
125+
class = "color-neutral-chip",
126+
div(class = "color-neutral-swatch", style = paste0("background:", hex_wb, ";")),
127+
tags$span(class = "color-neutral-hex", hex_wb)
128+
)
129+
})
130+
)
131+
}
111132
),
112133
# Right: backing gradient + fan cards
113134
div(
@@ -191,7 +212,12 @@ app_server <- function(input, output, session) {
191212
tags$ol(
192213
class = "result-link-list",
193214
lapply(rv$site$fonts, function(font) {
194-
tags$li(font[1])
215+
family_raw <- font[["family"]]
216+
font_name <- strsplit(as.character(family_raw), ":")[[1]][1]
217+
font_url <- paste0("https://fonts.google.com/specimen/", gsub(" ", "+", font_name))
218+
tags$li(
219+
tags$a(href = font_url, target = "_blank", font_name)
220+
)
195221
})
196222
)
197223
)
@@ -226,7 +252,7 @@ app_server <- function(input, output, session) {
226252

227253
# 5. All colors section
228254
div(
229-
class = "result-section result-section-last",
255+
class = "result-section",
230256
div(
231257
class = "result-section-label",
232258
tags$span(class = "result-section-tag", "All colors"),
@@ -235,8 +261,107 @@ app_server <- function(input, output, session) {
235261
),
236262
div(
237263
class = "result-section-card p-0",
264+
div(
265+
class = "all-colors-toolbar",
266+
tags$span(class = "all-colors-sort-label", "Sort"),
267+
radioButtons(
268+
inputId = "color_sort",
269+
label = NULL,
270+
choices = c("Frequency" = "freq", "Hue" = "hue"),
271+
selected = "freq",
272+
inline = TRUE
273+
)
274+
),
238275
uiOutput("all_colors")
239276
)
277+
),
278+
279+
# 6. Live preview section
280+
div(
281+
class = "result-section result-section-last",
282+
div(
283+
class = "result-section-label",
284+
tags$span(class = "result-section-tag", "Preview"),
285+
tags$h3(class = "result-section-title", "Live mockup"),
286+
tags$p(
287+
class = "result-section-desc",
288+
HTML("Bootstrap components rendered with the extracted theme.<br>What your app could look like.")
289+
)
290+
),
291+
div(
292+
class = "result-section-card p-0 overflow-hidden",
293+
uiOutput("preview_controls"),
294+
div(
295+
class = "theme-preview-wrap",
296+
# Navbar
297+
tags$nav(
298+
class = "navbar bg-primary px-3 py-2",
299+
`data-bs-theme` = "dark",
300+
div(
301+
class = "container-fluid p-0",
302+
tags$a(class = "navbar-brand text-white fw-semibold me-4", rv$site$domain),
303+
div(
304+
class = "navbar-nav flex-row gap-3",
305+
tags$a(class = "nav-link active text-white", href = "#", "Home"),
306+
tags$a(class = "nav-link text-white opacity-75", href = "#", "Plots"),
307+
tags$a(class = "nav-link text-white opacity-75", href = "#", "Data")
308+
)
309+
)
310+
),
311+
# Body
312+
div(
313+
class = "theme-preview-body container-fluid py-3 px-3",
314+
div(
315+
class = "row g-3",
316+
# Card 1
317+
div(
318+
class = "col-md-6",
319+
div(
320+
class = "card",
321+
div(class = "card-header fw-semibold", "Dashboard"),
322+
div(
323+
class = "card-body",
324+
tags$p(class = "card-text text-muted small mb-3",
325+
"Your Shiny app, styled with the extracted palette."),
326+
div(
327+
class = "d-flex flex-wrap gap-2",
328+
tags$button(class = "btn btn-primary btn-sm", "Primary"),
329+
tags$button(class = "btn btn-secondary btn-sm", "Secondary"),
330+
tags$button(class = "btn btn-outline-primary btn-sm", "Outline")
331+
)
332+
)
333+
)
334+
),
335+
# Card 2
336+
div(
337+
class = "col-md-6",
338+
div(
339+
class = "card",
340+
div(class = "card-header fw-semibold", "Controls"),
341+
div(
342+
class = "card-body",
343+
div(
344+
class = "mb-3",
345+
tags$label(class = "form-label small text-muted", "Select a variable"),
346+
tags$select(
347+
class = "form-select form-select-sm",
348+
tags$option("Option A"),
349+
tags$option("Option B"),
350+
tags$option("Option C")
351+
)
352+
),
353+
div(
354+
class = "input-group input-group-sm",
355+
tags$input(type = "number", class = "form-control", value = "42"),
356+
tags$button(class = "btn btn-primary", "Run")
357+
)
358+
)
359+
)
360+
)
361+
)
362+
)
363+
)
364+
)
240365
)
241366
)
242367
})
@@ -247,9 +372,86 @@ app_server <- function(input, output, session) {
247372
rv$result
248373
})
249374

375+
output$preview_controls <- renderUI({
376+
req(rv$site, rv$preview_primary, rv$preview_secondary)
377+
colors <- rv$site$top_colors$top_colors$Color
378+
379+
make_chips <- function(role, current) {
380+
div(
381+
class = "theme-preview-editor-row",
382+
tags$span(class = "preview-role-label", role),
383+
div(
384+
class = "preview-palette",
385+
lapply(colors, function(hex) {
386+
div(
387+
class = paste0("preview-chip", if (hex == current) " preview-chip-active" else ""),
388+
style = paste0("background:", hex, ";"),
389+
onclick = paste0(
390+
'Shiny.setInputValue("preview_',
391+
tolower(role),
392+
'", "', hex, '", {priority: "event"})'
393+
)
394+
)
395+
})
396+
)
397+
)
398+
}
399+
400+
div(
401+
class = "theme-preview-editor",
402+
make_chips("Primary", rv$preview_primary),
403+
make_chips("Secondary", rv$preview_secondary)
404+
)
405+
})
406+
407+
observeEvent(input$preview_primary, {
408+
req(rv$site)
409+
rv$preview_primary <- input$preview_primary
410+
session$setCurrentTheme(
411+
bslib::bs_theme_update(
412+
rv$site$shiny_theme,
413+
primary = rv$preview_primary,
414+
secondary = rv$preview_secondary
415+
)
416+
)
417+
})
418+
419+
observeEvent(input$preview_secondary, {
420+
req(rv$site)
421+
rv$preview_secondary <- input$preview_secondary
422+
session$setCurrentTheme(
423+
bslib::bs_theme_update(
424+
rv$site$shiny_theme,
425+
primary = rv$preview_primary,
426+
secondary = rv$preview_secondary
427+
)
428+
)
429+
})
430+
250431
output$all_colors <- renderUI({
251432
req(rv$site$all_colors)
252-
df <- rv$site$all_colors
433+
df <- rv$site$all_colors
434+
435+
if (!is.null(input$color_sort) && input$color_sort == "hue") {
436+
hex_to_hue <- function(hex) {
437+
hex <- sub("^#", "", hex)
438+
r <- strtoi(substr(hex, 1, 2), 16L) / 255
439+
g <- strtoi(substr(hex, 3, 4), 16L) / 255
440+
b <- strtoi(substr(hex, 5, 6), 16L) / 255
441+
max_c <- max(r, g, b)
442+
min_c <- min(r, g, b)
443+
if (max_c == min_c) return(0)
444+
delta <- max_c - min_c
445+
if (max_c == r) h <- (g - b) / delta
446+
else if (max_c == g) h <- 2 + (b - r) / delta
447+
else h <- 4 + (r - g) / delta
448+
h <- h * 60
449+
if (h < 0) h <- h + 360
450+
h
451+
}
452+
df <- df[order(sapply(df$Color, hex_to_hue)), ]
453+
}
454+
253455
max_count <- max(df$Count)
254456

255457
div(
@@ -269,6 +471,11 @@ app_server <- function(input, output, session) {
269471
div(class = "color-swatch-bar", style = paste0("width:", bar, "%;"))
270472
),
271473
tags$span(class = "color-swatch-count", paste0(cnt, "\u00d7"))
474+
),
475+
tags$button(
476+
class = "swatch-copy-btn",
477+
onclick = paste0('copySwatchHex("', hex, '", this)'),
478+
"Copy"
272479
)
273480
)
274481
})

R/css2r.R

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,19 @@ css2r <- R6Class(
255255
)
256256
)
257257

258+
# Also scan downloaded CSS for @import Google Fonts URLs
259+
if (!is.null(self$css_content)) {
260+
css_import_matches <- regmatches(
261+
self$css_content,
262+
gregexpr(
263+
pattern = "https://fonts\\.googleapis\\.com[^\"')\\s]+",
264+
text = self$css_content,
265+
perl = TRUE
266+
)
267+
)[[1]]
268+
google_font_links <- unique(c(google_font_links, css_import_matches))
269+
}
270+
258271
if (length(google_font_links) > 0) {
259272
google_fonts_params <- private$extract_google_font_params(
260273
links = google_font_links

ROADMAP.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ROADMAP
2+
3+
## Limitations moteur (`R/css2r.R`)
4+
5+
- Regex hex 6 chiffres uniquement → manque `rgb()`, `hsl()`, `#ABC`, variables CSS (`--my-color`)
6+
- Top 4 couleurs hardcodé (line 229 : `head(other_colors, 4)`) → rendre configurable
7+
- CSS même domaine uniquement → CDN externes exclus (Bootstrap, Tailwind…)
8+
- Google Fonts uniquement (`fonts.googleapis.com`) → pas de `@font-face`, Adobe Fonts
9+
- Balises `<link>` uniquement → pas de `<style>` inline
10+
- Pas de déduplication perceptuelle des couleurs proches
11+
12+
## Features UX — Quick wins
13+
14+
- [x] Bouton Copy sur chaque swatch hex dans la grille "All colors"
15+
- [x] Afficher les couleurs blanc/noir extraites (actuellement cachées dans `white_black`)
16+
- [x] Lien direct Google Fonts pour chaque police détectée
17+
- [x] Filtre / tri dans la grille des couleurs (par fréquence, par teinte)
18+
19+
## Features UX — Effort moyen
20+
21+
- Historique des URLs analysées (stocké en session)
22+
- Indicateur de progression étape par étape du pipeline
23+
- Vérification WCAG : afficher le ratio de contraste des couleurs choisies
24+
- Messages d'erreur enrichis (code HTTP, raison exacte de l'échec)
25+
26+
## Features majeures
27+
28+
- Export : thème en `.R`, couleurs en `.json` / `.csv`, palette en PNG
29+
- Génération d'un thème dark mode à partir des mêmes couleurs
30+
- [x] Preview live : mini-mockup Shiny (bouton, card, navbar) avec le thème généré
31+
- Analyse multi-URL : comparer les palettes de deux sites côte à côte
32+
- Éditeur de palette : réordonner ou exclure des couleurs avant de générer le code

0 commit comments

Comments
 (0)