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
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: css2r
Title: An Amazing Shiny App
Version: 0.1.0
Version: 0.2.0
Authors@R:
person("Arthur", "Bréant", , "arthur@thinkr.fr", role = c("aut", "cre"))
Description: What the package does (one paragraph).
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# css2r 0.2.0

# css2r 0.1.0

* Update UI with new features
Expand Down
217 changes: 212 additions & 5 deletions R/app_server.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#' @import shiny
#' @noRd
app_server <- function(input, output, session) {
rv <- reactiveValues(site = NULL)
rv <- reactiveValues(site = NULL, preview_primary = NULL, preview_secondary = NULL)

observeEvent(input$analyze_btn, {
rv$result <- NULL
Expand Down Expand Up @@ -66,6 +66,12 @@ app_server <- function(input, output, session) {
observeEvent(rv$continue, {
req(rv$site)

session$setCurrentTheme(rv$site$shiny_theme)

top_colors_df_init <- rv$site$top_colors$top_colors
rv$preview_primary <- top_colors_df_init$Color[1]
rv$preview_secondary <- if (nrow(top_colors_df_init) > 1) top_colors_df_init$Color[2] else top_colors_df_init$Color[1]

session$sendCustomMessage(
type = "apply_gradient",
message = list(
Expand Down Expand Up @@ -107,7 +113,22 @@ app_server <- function(input, output, session) {
tags$p(
class = "color-section-total",
paste0(nrow(rv$site$all_colors), " unique colors in total")
)
),
if (!is.null(rv$site$top_colors$white_black) && nrow(rv$site$top_colors$white_black) > 0) {
wb <- rv$site$top_colors$white_black
div(
class = "color-section-neutrals",
tags$span(class = "color-section-neutrals-label", "Neutrals"),
lapply(seq_len(nrow(wb)), function(j) {
hex_wb <- wb$Color[j]
div(
class = "color-neutral-chip",
div(class = "color-neutral-swatch", style = paste0("background:", hex_wb, ";")),
tags$span(class = "color-neutral-hex", hex_wb)
)
})
)
}
),
# Right: backing gradient + fan cards
div(
Expand Down Expand Up @@ -191,7 +212,12 @@ app_server <- function(input, output, session) {
tags$ol(
class = "result-link-list",
lapply(rv$site$fonts, function(font) {
tags$li(font[1])
family_raw <- font[["family"]]
font_name <- strsplit(as.character(family_raw), ":")[[1]][1]
font_url <- paste0("https://fonts.google.com/specimen/", gsub(" ", "+", font_name))
tags$li(
tags$a(href = font_url, target = "_blank", font_name)
)
})
)
)
Expand Down Expand Up @@ -226,7 +252,7 @@ app_server <- function(input, output, session) {

# 5. All colors section
div(
class = "result-section result-section-last",
class = "result-section",
div(
class = "result-section-label",
tags$span(class = "result-section-tag", "All colors"),
Expand All @@ -235,8 +261,107 @@ app_server <- function(input, output, session) {
),
div(
class = "result-section-card p-0",
div(
class = "all-colors-toolbar",
tags$span(class = "all-colors-sort-label", "Sort"),
radioButtons(
inputId = "color_sort",
label = NULL,
choices = c("Frequency" = "freq", "Hue" = "hue"),
selected = "freq",
inline = TRUE
)
),
uiOutput("all_colors")
)
),

# 6. Live preview section
div(
class = "result-section result-section-last",
div(
class = "result-section-label",
tags$span(class = "result-section-tag", "Preview"),
tags$h3(class = "result-section-title", "Live mockup"),
tags$p(
class = "result-section-desc",
HTML("Bootstrap components rendered with the extracted theme.<br>What your app could look like.")
)
),
div(
class = "result-section-card p-0 overflow-hidden",
uiOutput("preview_controls"),
div(
class = "theme-preview-wrap",
# Navbar
tags$nav(
class = "navbar bg-primary px-3 py-2",
`data-bs-theme` = "dark",
div(
class = "container-fluid p-0",
tags$a(class = "navbar-brand text-white fw-semibold me-4", rv$site$domain),
div(
class = "navbar-nav flex-row gap-3",
tags$a(class = "nav-link active text-white", href = "#", "Home"),
tags$a(class = "nav-link text-white opacity-75", href = "#", "Plots"),
tags$a(class = "nav-link text-white opacity-75", href = "#", "Data")
)
)
),
# Body
div(
class = "theme-preview-body container-fluid py-3 px-3",
div(
class = "row g-3",
# Card 1
div(
class = "col-md-6",
div(
class = "card",
div(class = "card-header fw-semibold", "Dashboard"),
div(
class = "card-body",
tags$p(class = "card-text text-muted small mb-3",
"Your Shiny app, styled with the extracted palette."),
div(
class = "d-flex flex-wrap gap-2",
tags$button(class = "btn btn-primary btn-sm", "Primary"),
tags$button(class = "btn btn-secondary btn-sm", "Secondary"),
tags$button(class = "btn btn-outline-primary btn-sm", "Outline")
)
)
)
),
# Card 2
div(
class = "col-md-6",
div(
class = "card",
div(class = "card-header fw-semibold", "Controls"),
div(
class = "card-body",
div(
class = "mb-3",
tags$label(class = "form-label small text-muted", "Select a variable"),
tags$select(
class = "form-select form-select-sm",
tags$option("Option A"),
tags$option("Option B"),
tags$option("Option C")
)
),
div(
class = "input-group input-group-sm",
tags$input(type = "number", class = "form-control", value = "42"),
tags$button(class = "btn btn-primary", "Run")
)
)
)
)
)
)
)
)
)
)
})
Expand All @@ -247,9 +372,86 @@ app_server <- function(input, output, session) {
rv$result
})

output$preview_controls <- renderUI({
req(rv$site, rv$preview_primary, rv$preview_secondary)
colors <- rv$site$top_colors$top_colors$Color

make_chips <- function(role, current) {
div(
class = "theme-preview-editor-row",
tags$span(class = "preview-role-label", role),
div(
class = "preview-palette",
lapply(colors, function(hex) {
div(
class = paste0("preview-chip", if (hex == current) " preview-chip-active" else ""),
style = paste0("background:", hex, ";"),
onclick = paste0(
'Shiny.setInputValue("preview_',
tolower(role),
'", "', hex, '", {priority: "event"})'
)
)
})
)
)
}

div(
class = "theme-preview-editor",
make_chips("Primary", rv$preview_primary),
make_chips("Secondary", rv$preview_secondary)
)
})

observeEvent(input$preview_primary, {
req(rv$site)
rv$preview_primary <- input$preview_primary
session$setCurrentTheme(
bslib::bs_theme_update(
rv$site$shiny_theme,
primary = rv$preview_primary,
secondary = rv$preview_secondary
)
)
})

observeEvent(input$preview_secondary, {
req(rv$site)
rv$preview_secondary <- input$preview_secondary
session$setCurrentTheme(
bslib::bs_theme_update(
rv$site$shiny_theme,
primary = rv$preview_primary,
secondary = rv$preview_secondary
)
)
})

output$all_colors <- renderUI({
req(rv$site$all_colors)
df <- rv$site$all_colors
df <- rv$site$all_colors

if (!is.null(input$color_sort) && input$color_sort == "hue") {
hex_to_hue <- function(hex) {
hex <- sub("^#", "", hex)
r <- strtoi(substr(hex, 1, 2), 16L) / 255
g <- strtoi(substr(hex, 3, 4), 16L) / 255
b <- strtoi(substr(hex, 5, 6), 16L) / 255
max_c <- max(r, g, b)
min_c <- min(r, g, b)
if (max_c == min_c) return(0)
delta <- max_c - min_c
if (max_c == r) h <- (g - b) / delta
else if (max_c == g) h <- 2 + (b - r) / delta
else h <- 4 + (r - g) / delta
h <- h * 60
if (h < 0) h <- h + 360
h
}
df <- df[order(sapply(df$Color, hex_to_hue)), ]
}

max_count <- max(df$Count)

div(
Expand All @@ -269,6 +471,11 @@ app_server <- function(input, output, session) {
div(class = "color-swatch-bar", style = paste0("width:", bar, "%;"))
),
tags$span(class = "color-swatch-count", paste0(cnt, "\u00d7"))
),
tags$button(
class = "swatch-copy-btn",
onclick = paste0('copySwatchHex("', hex, '", this)'),
"Copy"
)
)
})
Expand Down
13 changes: 13 additions & 0 deletions R/css2r.R
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,19 @@ css2r <- R6Class(
)
)

# Also scan downloaded CSS for @import Google Fonts URLs
if (!is.null(self$css_content)) {
css_import_matches <- regmatches(
self$css_content,
gregexpr(
pattern = "https://fonts\\.googleapis\\.com[^\"')\\s]+",
text = self$css_content,
perl = TRUE
)
)[[1]]
google_font_links <- unique(c(google_font_links, css_import_matches))
}

if (length(google_font_links) > 0) {
google_fonts_params <- private$extract_google_font_params(
links = google_font_links
Expand Down
32 changes: 32 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ROADMAP

## Limitations moteur (`R/css2r.R`)

- Regex hex 6 chiffres uniquement → manque `rgb()`, `hsl()`, `#ABC`, variables CSS (`--my-color`)
- Top 4 couleurs hardcodé (line 229 : `head(other_colors, 4)`) → rendre configurable
- CSS même domaine uniquement → CDN externes exclus (Bootstrap, Tailwind…)
- Google Fonts uniquement (`fonts.googleapis.com`) → pas de `@font-face`, Adobe Fonts
- Balises `<link>` uniquement → pas de `<style>` inline
- Pas de déduplication perceptuelle des couleurs proches

## Features UX — Quick wins

- [x] Bouton Copy sur chaque swatch hex dans la grille "All colors"
- [x] Afficher les couleurs blanc/noir extraites (actuellement cachées dans `white_black`)
- [x] Lien direct Google Fonts pour chaque police détectée
- [x] Filtre / tri dans la grille des couleurs (par fréquence, par teinte)

## Features UX — Effort moyen

- Historique des URLs analysées (stocké en session)
- Indicateur de progression étape par étape du pipeline
- Vérification WCAG : afficher le ratio de contraste des couleurs choisies
- Messages d'erreur enrichis (code HTTP, raison exacte de l'échec)

## Features majeures

- Export : thème en `.R`, couleurs en `.json` / `.csv`, palette en PNG
- Génération d'un thème dark mode à partir des mêmes couleurs
- [x] Preview live : mini-mockup Shiny (bouton, card, navbar) avec le thème généré
- Analyse multi-URL : comparer les palettes de deux sites côte à côte
- Éditeur de palette : réordonner ou exclure des couleurs avant de générer le code
Loading