From 8cda5e84aef47e42df6b4b179784dd96cff19798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Wed, 25 Feb 2026 15:27:57 +0100 Subject: [PATCH 1/3] feat: quick wins --- R/app_server.R | 63 ++++++++++++++++++++++-- R/css2r.R | 13 +++++ inst/app/www/custom.css | 106 ++++++++++++++++++++++++++++++++++++++++ inst/app/www/handler.js | 7 +++ 4 files changed, 186 insertions(+), 3 deletions(-) diff --git a/R/app_server.R b/R/app_server.R index ff2aed2..333f817 100644 --- a/R/app_server.R +++ b/R/app_server.R @@ -107,7 +107,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( @@ -191,7 +206,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) + ) }) ) ) @@ -235,6 +255,17 @@ 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") ) ) @@ -249,7 +280,28 @@ app_server <- function(input, output, session) { 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( @@ -269,6 +321,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" ) ) }) diff --git a/R/css2r.R b/R/css2r.R index cd98a27..aa6762b 100644 --- a/R/css2r.R +++ b/R/css2r.R @@ -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 diff --git a/inst/app/www/custom.css b/inst/app/www/custom.css index 4f173d8..e6f83e8 100644 --- a/inst/app/www/custom.css +++ b/inst/app/www/custom.css @@ -384,6 +384,7 @@ pre { } .color-swatch-item { + position: relative; border-radius: 8px; overflow: hidden; border: 1px solid #ebebef; @@ -436,6 +437,111 @@ pre { letter-spacing: 0.02em; } +/* ─── Swatch copy button ─────────────────────────────────────────────────── */ +.swatch-copy-btn { + position: absolute; + top: 4px; + right: 4px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + font-size: 0.58rem; + letter-spacing: 0.04em; + padding: 2px 6px; + cursor: pointer; + color: #444; + opacity: 0; + transition: opacity 0.15s ease; + line-height: 1.4; +} + +.color-swatch-item:hover .swatch-copy-btn { + opacity: 1; +} + +/* ─── Neutral color chips (white / black) ────────────────────────────────── */ +.color-section-neutrals { + margin-top: 1.1rem; + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.color-section-neutrals-label { + display: block; + font-size: 0.62rem; + font-family: 'SF Mono', 'Fira Mono', ui-monospace, monospace; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.28); + margin-bottom: 0.1rem; +} + +.color-neutral-chip { + display: inline-flex; + align-items: center; + gap: 0.5rem; + border-radius: 6px; + padding: 0.3rem 0.55rem; + border: 1px solid rgba(0, 0, 0, 0.10); + background: rgba(255, 255, 255, 0.7); + width: fit-content; +} + +.color-neutral-swatch { + width: 14px; + height: 14px; + border-radius: 3px; + border: 1px solid rgba(0, 0, 0, 0.15); + flex-shrink: 0; +} + +.color-neutral-hex { + font-family: 'SF Mono', 'Fira Mono', ui-monospace, monospace; + font-size: 0.68rem; + color: #444; +} + +/* ─── All colors sort toolbar ────────────────────────────────────────────── */ +.all-colors-toolbar { + padding: 0.65rem 1.25rem; + border-bottom: 1px solid #ebebef; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.all-colors-sort-label { + font-size: 0.62rem; + font-family: 'SF Mono', 'Fira Mono', ui-monospace, monospace; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.28); + white-space: nowrap; +} + +.all-colors-toolbar .shiny-input-container { + margin-bottom: 0 !important; +} + +.all-colors-toolbar .form-check { + margin-bottom: 0 !important; +} + +.all-colors-toolbar .form-check-label { + font-size: 0.75rem; + color: rgba(0, 0, 0, 0.45); + cursor: pointer; + letter-spacing: 0.02em; +} + +.all-colors-toolbar .form-check-input:checked + .form-check-label { + color: #111; + font-weight: 500; +} + /* ─── Animations ─────────────────────────────────────────────────────────── */ .result-section { opacity: 0; diff --git a/inst/app/www/handler.js b/inst/app/www/handler.js index 3d04166..33ddab5 100644 --- a/inst/app/www/handler.js +++ b/inst/app/www/handler.js @@ -1,3 +1,10 @@ +function copySwatchHex(hex, btn) { + navigator.clipboard.writeText(hex).then(function() { + btn.innerText = '\u2713'; + setTimeout(function() { btn.innerText = 'Copy'; }, 1500); + }); +} + function copyShinnyCode() { var code = document.getElementById('shiny_code').innerText; navigator.clipboard.writeText(code).then(function() { From 7c5ae95d4d783192c7da0c8a9ffc946327c8796e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Wed, 25 Feb 2026 15:41:07 +0100 Subject: [PATCH 2/3] feat: live preview --- R/app_server.R | 154 +++++++++++++++++++++++++++++++++++++++- inst/app/www/custom.css | 59 +++++++++++++++ 2 files changed, 211 insertions(+), 2 deletions(-) diff --git a/R/app_server.R b/R/app_server.R index 333f817..afabaf1 100644 --- a/R/app_server.R +++ b/R/app_server.R @@ -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 @@ -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( @@ -246,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"), @@ -268,6 +274,94 @@ app_server <- function(input, output, session) { ), 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.
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") + ) + ) + ) + ) + ) + ) + ) + ) ) ) }) @@ -278,6 +372,62 @@ 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 diff --git a/inst/app/www/custom.css b/inst/app/www/custom.css index e6f83e8..c05616a 100644 --- a/inst/app/www/custom.css +++ b/inst/app/www/custom.css @@ -552,6 +552,7 @@ pre { .result-section:nth-child(3) { animation-delay: 0.2s; } .result-section:nth-child(4) { animation-delay: 0.3s; } .result-section:nth-child(5) { animation-delay: 0.4s; } +.result-section:nth-child(6) { animation-delay: 0.5s; } @keyframes fadeUp { from { opacity: 0; transform: translateY(14px); } @@ -563,6 +564,64 @@ pre { to { transform: translateY(0); opacity: 1; } } +/* ─── Live theme preview ─────────────────────────────────────────────────── */ +.theme-preview-body { + background: var(--bs-body-bg, #ffffff); +} + +/* ─── Preview color editor ───────────────────────────────────────────────── */ +.theme-preview-editor { + padding: 0.7rem 1rem; + border-bottom: 1px solid #ebebef; + background: #fafafa; + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.theme-preview-editor-row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.preview-role-label { + font-size: 0.62rem; + font-family: 'SF Mono', 'Fira Mono', ui-monospace, monospace; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.28); + width: 58px; + flex-shrink: 0; +} + +.preview-palette { + display: flex; + gap: 0.35rem; + align-items: center; +} + +.preview-chip { + width: 22px; + height: 22px; + border-radius: 50%; + cursor: pointer; + border: 2px solid transparent; + box-sizing: border-box; + transition: transform 0.15s ease, box-shadow 0.15s ease; + flex-shrink: 0; +} + +.preview-chip:hover { + transform: scale(1.18); +} + +.preview-chip-active { + box-shadow: 0 0 0 2px #fff, 0 0 0 4px rgba(0, 0, 0, 0.35); + transform: scale(1.12); +} + /* ─── Footer ─────────────────────────────────────────────────────────────── */ .app-footer { display: flex; From 5372aa0e2ae9f0be4bcce2fc7f1f8eb46a2f3766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Wed, 25 Feb 2026 15:42:06 +0100 Subject: [PATCH 3/3] chore: bump version --- DESCRIPTION | 2 +- NEWS.md | 2 ++ ROADMAP.md | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 ROADMAP.md diff --git a/DESCRIPTION b/DESCRIPTION index 5f16934..6d0ca2a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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). diff --git a/NEWS.md b/NEWS.md index 25f14f9..d6845ff 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,5 @@ +# css2r 0.2.0 + # css2r 0.1.0 * Update UI with new features diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..a1dc34e --- /dev/null +++ b/ROADMAP.md @@ -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 `` uniquement → pas de `