| Title: | Shared 'Shiny' User Interface Components for KIT Research Tools |
|---|---|
| Description: | The shared user interface layer of the KIT research tools built with 'shiny': a 'bslib' theme based on Bootswatch Brite, the KIT style sheets and logos, a page wrapper with the KIT header, and documented components (page guide, buttons, download buttons, callouts, cloud-version banner and footer, status boxes, details rows, code panels, step cards, checklists, step tabs, diagrams, tables and a 'plotly' chart style), a skeleton for tabs with one run button (page order and status flow), plus a few server-side helpers (run gate, scroll to result, tab states, notifications, number formatting, user-facing errors), value objects for status boxes, checklists and code, 'shiny' modules for the stateful pieces (step cards, step tabs, run gate, code panel) and a scaffold for new apps. Load it with one call to kit_page() and build the page from kit_*() components. The logo and the colour palette come from a brand object (kit_brand()), so projects other than KIT can use their own. |
| Authors: | Jie-Wen Tsai [aut, cre] |
| Maintainer: | Jie-Wen Tsai <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.7.0 |
| Built: | 2026-11-01 11:52:09 UTC |
| Source: | https://github.com/jiewenTsai/kitDesign |
The message should be plain-language Chinese that says what went wrong and how to fix it;
kit_error_status() shows it as written in the status box, with no technical details. Any other,
unexpected error is wrapped as "…時發生沒有預期到的問題" ("an unexpected problem occurred while …"),
with the original message tucked away.
kit_abort(..., class = NULL, data = list())kit_abort(..., class = NULL, data = list())
... |
The message (joined with |
class |
An extra condition class (the caller can handle it separately, e.g. to show a confirmation dialog). |
data |
Other fields to attach to the condition (a named list). |
Does not return: throws a kit_user_error error.
e <- tryCatch(kit_abort("至少要 ", 2, " 個波次的檔案。"), error = identity) # "Need files from at least 2 waves." class(e) conditionMessage(e)e <- tryCatch(kit_abort("至少要 ", 2, " 個波次的檔案。"), error = identity) # "Need files from at least 2 waves." class(e) conditionMessage(e)
Options in a settings area that are rarely changed (text column matching, ID type conversion, reverse-scored item rules, separators…). A one-line small-print title; when opened, a faint line on the left sets it apart from the other settings (no box).
kit_advanced(..., title = "進階設定", open = FALSE, id = NULL)kit_advanced(..., title = "進階設定", open = FALSE, id = NULL)
... |
The settings inside. |
title |
The collapsible title (can be a tag, e.g. followed by the current settings). |
open |
Whether it starts open. |
id |
The id of the |
<details class="kit-advanced">.
kit_advanced(shiny::checkboxInput("trim", "比對前去掉前後空白", TRUE)) # "Trim spaces before matching"kit_advanced(shiny::checkboxInput("trim", "比對前去掉前後空白", TRUE)) # "Trim spaces before matching"
The cloud version, the local run.bat, and the package's run_xxx() all take Version and Date
from DESCRIPTION, so all three show the same thing (file modification times are not used:
unzipping, installing, and deploying all change the mtime).
kit_app_description(package = NULL, path = NULL)kit_app_description(package = NULL, path = NULL)
package |
The app's package name, e.g. |
path |
Path to a DESCRIPTION file; when |
Search order:
path is given: read that DESCRIPTION file.
package is already loaded (library(), run_xxx(), load_all()): read the loaded copy.
DESCRIPTION in the working directory, then one and two levels up (in tests the working
directory is tests/testthat); when package is given, only a file whose Package: matches
counts. The cloud version and run.bat use runApp(folder), the package is not loaded, and
this is the copy that gets read.
package is installed (but not loaded): read the installed copy. This comes last because it
may be an old version.
list(version = , date = ); anything not found is NA.
kit_app_description("kitDesign")kit_app_description("kitDesign")
For example, a link from kitJoin to the cloud version of kitsUI. The link opens in a new tab
(target="_blank") and can be followed by a line of small print saying where it goes.
Whether to show the link (local version, cloud version) is up to the app.
kit_app_link(label, href, kind = "link", class = NULL, note = NULL)kit_app_link(label, href, kind = "link", class = NULL, note = NULL)
label |
The link text, e.g. "開啟 kitsUI" ("Open kitsUI"). |
href |
The URL. |
kind |
|
class |
An extra class for the link (e.g. |
note |
A line of small print after it, e.g. "(開啟網路上的雲端版 kitsUI)" ("(opens the
online cloud version of kitsUI)"); |
<span class="kit-app-link">.
kit_app_link("開啟 kitsUI", "https://example.org/kitsui/", kind = "secondary", note = "(開啟網路上的雲端版 kitsUI)")kit_app_link("開啟 kitsUI", "https://example.org/kitsui/", kind = "secondary", note = "(開啟網路上的雲端版 kitsUI)")
The version line always sits in the header; it is needed when reporting problems.
kit_app_meta( version = NULL, date = NULL, extra = NULL, show_kitdesign = FALSE, package = NULL )kit_app_meta( version = NULL, date = NULL, extra = NULL, show_kitdesign = FALSE, package = NULL )
version |
The version ("v" is added in front); |
date |
The date; |
extra |
Text to append (e.g. |
show_kitdesign |
Whether to append "kitDesign 0.1.0" ( |
package |
The app's package name, written before the version ("kitJoin v0.6.0"); |
<div class="kit-app-meta">; NULL when all three are missing.
kit_app_meta("0.5.0", "2026-10-31") kit_app_meta("0.6.0", "2026-10-31", package = "kitJoin")kit_app_meta("0.5.0", "2026-10-31") kit_app_meta("0.6.0", "2026-10-31", package = "kitJoin")
kitDesign looks like KIT by default. When another project or lab uses kitDesign, kit_brand()
swaps the logo and colors; typeface, borders, shadows, and how components look follow the design
language and stay the same. With no arguments it is KIT (kit_brand()), and pages look exactly as
before.
kit_brand( name = "KIT", logo = NULL, mark = NULL, logo_alt = NULL, colors = list() )kit_brand( name = "KIT", logo = NULL, mark = NULL, logo_alt = NULL, colors = list() )
name |
The brand name, e.g. |
logo |
Path to the header logo file (SVG or PNG); for |
mark |
Path to the browser tab icon file (SVG or PNG, square); when |
logo_alt |
Alt text for the logo; when |
colors |
Colors to replace: a named list or named character vector; names are in the table
above, values are |
Pass the brand to kit_page() (brand =), or set it once when the app starts:
options(kitDesign.brand = kit_brand(...)). After that kit_page(), kit_header(),
kit_theme(), kit_colors(), kit_chart_sequential(), and kit_plotly_style()
all use it (kit_current_brand()).
Colors (colors) use the design-language names; anything not given follows KIT:
| Name | CSS variable | KIT | Used for |
ink |
--nb-ink |
#141414 |
text, borders, shadows |
page |
--nb-page |
#faf7f0 |
page background |
card |
--nb-card |
#ffffff |
cards, inputs, tables |
accent |
--nb-lime |
#c6d033 |
main color block: header, primary button, current tab |
accent_light |
--nb-bg-new |
#e8f0a0 |
light main color: new columns in diagrams, light end of the color scale |
primary |
--nb-green |
#006835 |
links, checkboxes, progress bars, dark end of the color scale |
primary_hover |
--nb-green-hover |
#004d27 |
link hover |
success |
--nb-mint |
#b9f0a8 |
【完成】 (done) |
warning |
--nb-yellow |
#ffde59 |
【完成,但請注意】 (done, but take note) |
danger |
--nb-salmon |
#ff9b8f |
【停下來】 (stopped), danger button |
info |
--nb-sky |
#a8dcff |
info callout |
If only accent is given, accent_light is derived from it (mixed with white); if only primary
is given, primary_hover is derived from it (darkened). Text on color blocks is always ink: when
choosing accent and the four status colors, check that ink on them has at least 4.5:1 contrast;
primary is a text color and also needs 4.5:1 on page.
Logo: logo is the full header logo, mark is the small browser tab icon (square; when not
given, logo is used). Both are paths to SVG or PNG files; the files are read when the brand is
created, so changing the working directory later does not matter.
In Shiny they are served by an htmlDependency containing only these two files (the folder they live
in is not exposed); with kit_header(logo = "inline") they are written as a data URI.
With name = "KIT" and no logo, the KIT logo is used; with any other name and no logo, the
header has no logo. logo = FALSE never shows a logo.
A kit_brand object (a list: name, logo_alt, colors (11 #rrggbb values), and the
logo files).
kit_current_brand(), kit_page(), kit_theme(), kit_chart_sequential()
kit_brand() # Another lab: blue colors, its own logo demo <- kit_brand( "示範實驗室", logo = system.file("examples", "brand", "demo-logo.svg", package = "kitDesign"), mark = system.file("examples", "brand", "demo-mark.svg", package = "kitDesign"), colors = list(accent = "#9cc0ff", primary = "#1f4fa3", page = "#f5f7fa") ) demo if (interactive()) { options(kitDesign.brand = demo) # use this brand for the whole app kit_gallery() }kit_brand() # Another lab: blue colors, its own logo demo <- kit_brand( "示範實驗室", logo = system.file("examples", "brand", "demo-logo.svg", package = "kitDesign"), mark = system.file("examples", "brand", "demo-mark.svg", package = "kitDesign"), colors = list(accent = "#9cc0ff", primary = "#1f4fa3", page = "#f5f7fa") ) demo if (interactive()) { options(kitDesign.brand = demo) # use this brand for the whole app kit_gallery() }
When you build a button yourself (e.g. downloadButton(), <a class="btn">), use this class
so it looks the same as kit_button().
kit_btn_class(kind = c("secondary", "primary", "danger"))kit_btn_class(kind = c("secondary", "primary", "danger"))
kind |
|
A class string, e.g. "btn-sm btn-outline-secondary".
kit_btn_class("secondary")kit_btn_class("secondary")
A shiny::actionButton() with a class set by the button kind:
kit_button(id, label, kind = "secondary", ..., class = NULL)kit_button(id, label, kind = "secondary", ..., class = NULL)
id |
The button's input id. |
label |
The text on the button. |
kind |
|
... |
Other arguments passed to |
class |
An extra class. |
"primary": primary button, lime background, large text, card shadow. One per page, plus the
"下一步" ("Next step") at the bottom.
"secondary": secondary button, white background, thin border, no shadow. For download,
select all, example files, move up/down, copy.
"danger": danger button, salmon background. For delete, remove, skip this round.
For a regular button (white background, black border, small shadow, e.g. "對,下一步" ("Yes,
next step")) use shiny::actionButton() directly.
Start the text with a verb and say clearly what pressing it does; if it leaves the page, add
"→" after the text.
<button class="btn ...">.
kit_button("run", "執行串接", "primary") # "Run join" kit_button("all", "全選") # "Select all" kit_button("drop", "這一回合不做", "danger") # "Skip this round"kit_button("run", "執行串接", "primary") # "Run join" kit_button("all", "全選") # "Select all" kit_button("drop", "這一回合不做", "danger") # "Skip this round"
A reminder the user should know before pressing a button. It is not a result, so it has no shadow. At most one per section; if it can go in the status box, don't add a separate callout; if there is a button, put the button outside the box.
kit_callout(..., warn = FALSE, class = NULL)kit_callout(..., warn = FALSE, class = NULL)
... |
The content of the box. |
warn |
|
class |
An extra class. |
<div class="kit-callout"> (plus kit-callout-warn for caution).
kit_callout("外部連結不適用:這一回合只有一個世代,這不是錯誤。") # "External linking does not apply: this round has only one cohort. This is not an error." kit_callout("資料裡有疑似遺漏值代碼,建議先到「遺漏值處理」。", warn = TRUE) # "The data seem to contain missing-value codes; go to 'Missing values' first."kit_callout("外部連結不適用:這一回合只有一個世代,這不是錯誤。") # "External linking does not apply: this round has only one cohort. This is not an error." kit_callout("資料裡有疑似遺漏值代碼,建議先到「遺漏值處理」。", warn = TRUE) # "The data seem to contain missing-value codes; go to 'Missing values' first."
Colorblind-friendly Okabe–Ito. Cohorts, scales, and scores take colors in order: blue, orange,
green, reddish purple, sky blue, vermillion; past 6 categories it starts over. For reference
lines, bands, and points with no judgment, use kit_chart_neutral().
kit_chart_colors(n = NULL)kit_chart_colors(n = NULL)
n |
When |
A character vector of colors.
kit_chart_colors() kit_chart_colors(3)kit_chart_colors() kit_chart_colors(3)
Reference lines, bands, and points with no judgment.
kit_chart_neutral()kit_chart_neutral()
"#7F7F7F".
kit_chart_neutral()kit_chart_neutral()
Very light lime → light lime (bg-new) → KIT green (nb-green): larger values are darker. For
heatmaps and color bars (e.g. pass rates in a response design plot). The minimum is not white,
so 0 looks different from an empty cell with no data (white background). kit_plotly_style()
switches heatmaps to this scale by default.
kit_chart_sequential(brand = kit_current_brand())kit_chart_sequential(brand = kit_current_brand())
brand |
The brand ( |
The scale comes from the brand (kit_brand()): 0 is a very light color made by mixing
accent_light with white, 0.2 is accent_light, and 0.2 to 1 interpolates in Lab space from
accent_light to primary. For the KIT brand these are the values above. Categorical colors
(kit_chart_colors(), Okabe–Ito) do not change with the brand: they are colorblind-friendly.
A plotly colorscale: list(list(0, "#F3F7D3"), ..., list(1, "#006835")).
kit_plotly_style(), kit_chart_colors()
kit_chart_sequential() kit_chart_sequential(kit_brand("Demo", colors = list(accent = "#9cc0ff", primary = "#1f4fa3")))kit_chart_sequential() kit_chart_sequential(kit_brand("Demo", colors = list(accent = "#9cc0ff", primary = "#1f4fa3")))
An S3 value object (class kit_check): its content is checked when it is built; it has an
htmltools::as.tags() method, so putting it straight into a page renders <li class="kit-check">.
Usually placed inside kit_checklist().
kit_check(state = c("todo", "ok", "note"), title, why = "", actions = NULL) ## S3 method for class 'kit_check' as.tags(x, ...) ## S3 method for class 'kit_check' format(x, ...) ## S3 method for class 'kit_check' print(x, ...)kit_check(state = c("todo", "ok", "note"), title, why = "", actions = NULL) ## S3 method for class 'kit_check' as.tags(x, ...) ## S3 method for class 'kit_check' format(x, ...) ## S3 method for class 'kit_check' print(x, ...)
state |
|
title |
One sentence (a string). |
why |
The small print below (why); |
actions |
Buttons for the decision (a tag or list of tags); placed below the explanation. |
x |
A |
... |
Not used. |
A mark on the left, preceded by hidden text (read by screen readers):
"todo": ! needs a decision ("要決定:", "To decide:"). One bold sentence, a line of small
print below saying why, and the decision buttons below that.
"ok": ✓ fine, or already decided ("沒問題:", "OK:"). Font weight 500.
"note": i a reminder, no decision needed ("提醒:", "Note:").
Put the recommended option first and write "(建議)" ("(recommended)") on its button.
A kit_check object.
kit_check("todo", "有 3 題的分數從 1 開始", "kits 要的分數是 0、1、2…。") # "3 items have scores starting at 1" / "kits needs scores 0, 1, 2…" htmltools::as.tags(kit_check("ok", "45 題的分數都是整數")) # "All 45 items have integer scores"kit_check("todo", "有 3 題的分數從 1 開始", "kits 要的分數是 0、1、2…。") # "3 items have scores starting at 1" / "kits needs scores 0, 1, 2…" htmltools::as.tags(kit_check("ok", "45 題的分數都是整數")) # "All 45 items have integer scores"
Lists "things to decide" together with "things already fine", placed before a conversion or run.
An S3 value object (class kit_checklist, a list of kit_check()): put it straight into a page and
it renders as <ul class="kit-checklist">, one item per row, with the faintest line between rows.
kit_checklist(...) ## S3 method for class 'kit_checklist' as.tags(x, ...) ## S3 method for class 'kit_checklist' format(x, ...) ## S3 method for class 'kit_checklist' print(x, ...)kit_checklist(...) ## S3 method for class 'kit_checklist' as.tags(x, ...) ## S3 method for class 'kit_checklist' format(x, ...) ## S3 method for class 'kit_checklist' print(x, ...)
... |
|
x |
A |
A kit_checklist object.
cl <- kit_checklist( kit_check("todo", "有 3 題的分數從 1 開始", "kits 要的分數是 0、1、2…。", actions = kit_button("shift", "全部減 1(建議)")), kit_check("ok", "沒有疑似遺漏值代碼"), kit_check("note", "有 2 題沒有人作答,不會放進作答檔") ) # todo: "3 items have scores starting at 1" / "kits needs scores 0, 1, 2…" / button "Subtract 1 from all (recommended)" # ok: "No suspected missing-value codes" # note: "2 items have no responses and will not go into the response file" cl htmltools::as.tags(cl)cl <- kit_checklist( kit_check("todo", "有 3 題的分數從 1 開始", "kits 要的分數是 0、1、2…。", actions = kit_button("shift", "全部減 1(建議)")), kit_check("ok", "沒有疑似遺漏值代碼"), kit_check("note", "有 2 題沒有人作答,不會放進作答檔") ) # todo: "3 items have scores starting at 1" / "kits needs scores 0, 1, 2…" / button "Subtract 1 from all (recommended)" # ok: "No suspected missing-value codes" # note: "2 items have no responses and will not go into the response file" cl htmltools::as.tags(cl)
A yellow line reminding users that their data go to the cloud. The app writes the text itself (each app's server location and usage notes differ).
kit_cloud_banner(...)kit_cloud_banner(...)
... |
The banner content. |
<div class="alert alert-warning kit-cloud-banner" role="alert">.
kit_cloud_banner("此為雲端版:上傳的資料會傳送至雲端伺服器。含個資的資料請改用本機版。") # "This is the cloud version: uploaded data are sent to a cloud server. For data with personal information, use the local version."kit_cloud_banner("此為雲端版:上傳的資料會傳送至雲端伺服器。含個資的資料請改用本機版。") # "This is the cloud version: uploaded data are sent to a cloud server. For data with personal information, use the local version."
An S3 value object (class kit_code): code text plus a version name. It has an
htmltools::as.tags() method: put it straight into a page and it renders a line with "version
name (bold) + Copy" and the code box below, with the code first wrapped by kit_code_wrap() to
at most 70 columns per line (Chinese characters count as 2).
If a line is still too long after wrapping (e.g. a long string that cannot be broken), it gives
a warning (it does not stop), and the box scrolls sideways.
kit_code(code, label = "R", width = KIT_CODE_WIDTH) ## S3 method for class 'kit_code' as.tags(x, ...) ## S3 method for class 'kit_code' format(x, ...) ## S3 method for class 'kit_code' print(x, ...)kit_code(code, label = "R", width = KIT_CODE_WIDTH) ## S3 method for class 'kit_code' as.tags(x, ...) ## S3 method for class 'kit_code' format(x, ...) ## S3 method for class 'kit_code' print(x, ...)
code |
The code (a character vector; one element per line, or containing newlines). |
label |
The version name, e.g. "tidyverse 版" ("tidyverse version"), "base R 版" ("base R version"), "kits 版" ("kits version"). |
width |
Maximum columns per line; |
x |
A |
... |
Not used. |
A kit_code object.
kit_code_blocks(), kit_code_panel_ui()
x <- kit_code("joined <- dplyr::left_join(t1, t2, by = \"id\")", "tidyverse 版") x htmltools::as.tags(x)x <- kit_code("joined <- dplyr::left_join(t1, t2, by = \"id\")", "tidyverse 版") x htmltools::as.tags(x)
Each version is a kit_code(): a line with "version name (bold) + Copy" and the code box below
it; two versions are stacked vertically. Code lines are at most 70 columns (kit_code_wrap());
ligatures are always turned off.
kit_code_blocks(snips, note = KIT_CODE_NOTE, width = KIT_CODE_WIDTH)kit_code_blocks(snips, note = KIT_CODE_NOTE, width = KIT_CODE_WIDTH)
snips |
The code: a string, a |
note |
A one-line note at the top; |
width |
Maximum columns per line; |
kit_code_blocks(list( tidy = "joined <- dplyr::left_join(t1, t2, by = \"id\")", base = "joined <- merge(t1, t2, by = \"id\", all.x = TRUE)" )) kit_code_blocks(c("kits 版" = "fit <- kits::kits_calibrate(resp)"))kit_code_blocks(list( tidy = "joined <- dplyr::left_join(t1, t2, by = \"id\")", base = "joined <- merge(t1, t2, by = \"id\", all.x = TRUE)" )) kit_code_blocks(c("kits 版" = "fit <- kits::kits_calibrate(resp)"))
KIT code examples and notes are at most about 70 columns per line: Latin letters, digits, and symbols count as 1 column, Chinese characters (and other full-width characters) count as 2.
kit_code_cols(x)kit_code_cols(x)
x |
A character vector (one element per line). |
An integer vector: the number of columns in each line.
kit_code_cols(c("abc", "讀 CSV", "")) # Which lines are wider than 70 columns lines <- c("x <- 1", strrep("中", 36)) which(kit_code_cols(lines) > 70)kit_code_cols(c("abc", "讀 CSV", "")) # Which lines are wider than 70 columns lines <- c("x <- 1", strrep("中", 36)) which(kit_code_cols(lines) > 70)
A kit_detail() inside the details row (kit_details_row()), collapsed by default; the server
fills output_id with kit_render_code_panel(). With modules, use kit_code_panel_ui().
kit_code_panel(output_id, title = "用 R 怎麼做", open = FALSE)kit_code_panel(output_id, title = "用 R 怎麼做", open = FALSE)
output_id |
The |
title |
The collapsible title. |
open |
Whether it starts open (collapsed by default). |
<details class="kit-detail">.
kit_details_row(kit_detail("預覽", "…"), kit_code_panel("code"))kit_details_row(kit_detail("預覽", "…"), kit_code_panel("code"))
A collapsible block in the details row; the code is computed only when it is opened (Shiny does
not render outputs inside a collapsed block), and rendered as one or more kit_code() (at most
70 columns per line, with "Copy").
kit_code_panel_ui(id, title = "用 R 怎麼做", open = FALSE) kit_code_panel_server( id, code, note = KIT_CODE_NOTE, width = KIT_CODE_WIDTH, empty = KIT_CODE_EMPTY )kit_code_panel_ui(id, title = "用 R 怎麼做", open = FALSE) kit_code_panel_server( id, code, note = KIT_CODE_NOTE, width = KIT_CODE_WIDTH, empty = KIT_CODE_EMPTY )
id |
The module id. |
title |
The collapsible title. |
open |
Whether it starts open. |
code |
A function or reactive returning the code: a string, a |
note, width, empty
|
kit_code_panel_ui(): <details class="kit-detail">, placed last in
kit_details_row().
kit_code_panel_server(): a reactive with the current code (a list of kit_code(); NULL
when there is none yet).
kit_code_panel(), kit_render_code_panel() (low-level)
if (interactive()) { ui <- kit_page("KIT 示範", NULL, kit_details_row(kit_code_panel_ui("code"))) server <- function(input, output, session) { kit_code_panel_server("code", function() list(tidy = "x <- 1", base = "x <- 1")) } shiny::shinyApp(ui, server) }if (interactive()) { ui <- kit_page("KIT 示範", NULL, kit_details_row(kit_code_panel_ui("code"))) server <- function(input, output, session) { kit_code_panel_server("code", function() list(tidy = "x <- 1", base = "x <- 1")) } shiny::shinyApp(ui, server) }
"How to do it in R" wraps the code only right before showing it (kit_code() does this
automatically), so functions that generate code need not worry about width:
kit_code_wrap(code, width = KIT_CODE_WIDTH)kit_code_wrap(code, width = KIT_CODE_WIDTH)
code |
The code (a string; may have several lines, or be several strings). |
width |
Maximum columns per line. |
Comments: break at spaces or between Chinese characters; continuation lines also start with "# "; punctuation is not put at the start of a line.
Code: break after a comma inside parentheses, and align continuation lines just after that
parenthesis (a line break inside parentheses is always valid R).
If the parenthesis was opened on an earlier line (e.g. mutate( on its own line with the
arguments on the next), continuation lines use the same indent as this line.
If a trailing comment makes the line too wide, it is moved above the line.
After wrapping, the code is parsed again: if the expression differs from the original (or the original did not parse), the code is left unchanged. Places that cannot be broken (e.g. a very long string) are also left as is and scroll sideways in the code box.
A single string (returned as is when code has length 0).
code <- paste0( "x <- c(\"M03\", \"M03\", \"M03\", \"M36\", \"M36\", \"M36\", ", "\"M48\", \"M48\", \"M60\", \"M60\")" ) cat(kit_code_wrap(code))code <- paste0( "x <- c(\"M03\", \"M03\", \"M03\", \"M36\", \"M36\", \"M36\", ", "\"M48\", \"M48\", \"M60\", \"M60\")" ) cat(kit_code_wrap(code))
Same names as kit_colors() in kitJoin and kitsUI. Colors for UI components are in the CSS
tokens (var(--nb-lime) etc.; see the appendix of brand/en/design-language.md).
primary, accent, and subtitle come from the brand (primary, accent, and accent_light
in kit_brand()); primary_light is a lighter primary; the rest are fixed neutrals and older
alert colors.
kit_colors(brand = kit_current_brand())kit_colors(brand = kit_current_brand())
brand |
The brand ( |
A named list.
kit_colors()$primarykit_colors()$primary
options(kitDesign.brand = ), KIT when not setThe default brand for kitDesign functions (kit_page(), kit_header(), kit_theme(),
kit_colors(), kit_chart_sequential(), kit_plotly_style()).
An app only needs to set options(kitDesign.brand = kit_brand(...)) once at startup.
kit_current_brand()kit_current_brand()
A kit_brand object.
kit_current_brand()$namekit_current_brand()$name
Outputs inside a collapsed detail are computed only when it is opened: kit.js tells Shiny to
check again when it opens.
kit_detail(title, ..., open = FALSE)kit_detail(title, ..., open = FALSE)
title |
The title (a noun: "題目參數" ("Item parameters"), "這一回合的紀錄" ("Log for this round")). |
... |
The content. |
open |
Whether it starts open. |
<details class="kit-detail">.
kit_detail("預覽", shiny::tags$p("前 10 列")) # "Preview", "First 10 rows"kit_detail("預覽", shiny::tags$p("前 10 列")) # "Preview", "First 10 rows"
A row of collapsed small-print titles below the result (▸ 預覽 ▸ 這一回合的紀錄 ▸ 用 R 怎麼做; "Preview", "Log for this round", "How to do it in R"). The one that is open takes a full line, and its content has no box. "用 R 怎麼做" ("How to do it in R") always goes last.
kit_details_row(...)kit_details_row(...)
... |
|
<div class="kit-details">.
kit_details_row( kit_detail("預覽", "前 10 列"), kit_detail("這一回合的紀錄", "沒有提醒。") ) # "Preview" / "First 10 rows"; "Log for this round" / "No notes."kit_details_row( kit_detail("預覽", "前 10 列"), kit_detail("這一回合的紀錄", "沒有提醒。") ) # "Preview" / "First 10 rows"; "Log for this round" / "No notes."
A supporting element: no box, table as wide as its content, 1px thin lines, a light header (not the black header of large data tables); values in a monospace font. New columns get a light lime background and "(新)" ("(new)") after the name, so it does not rely on color alone; empty cells show a gray "(空)" ("(empty)").
kit_dg_table(df, title, added = NULL, row_header = FALSE)kit_dg_table(df, title, added = NULL, row_header = FALSE)
df |
A data.frame (values are converted to text); |
title |
The table name, placed in |
added |
Names of columns added in this step (e.g. |
row_header |
Whether column 1 holds row headers ( |
<table class="kit-dg-table">.
kit_dg_table(data.frame(id = 1:2, score = c(3, NA)), "第 1 波(t1)") # "Wave 1 (t1)"kit_dg_table(data.frame(id = 1:2, score = c(3, NA)), "第 1 波(t1)") # "Wave 1 (t1)"
Goes in the diagram of kit_tab_guide(). On narrow screens (≤ 576px) "before" is on top, the
arrow points down, and "after" is below.
Each small table has 2–3 rows and uses generic names (id, score, wave), not column names from a
particular database.
kit_diagram(before, after, caption, arrow = "→")kit_diagram(before, after, caption, arrow = "→")
before |
The small table(s) before: one |
after |
The small table after: one |
caption |
One plain-language line ( |
arrow |
The arrow in the middle. |
<figure class="kit-diagram">.
kit_diagram( list(kit_dg_table(data.frame(id = 1:2, t1 = c(3, 5)), "第 1 波"), kit_dg_table(data.frame(id = c(1, 3), t2 = c(4, 6)), "第 2 波")), kit_dg_table(data.frame(id = 1:3, t1 = c(3, 5, NA), t2 = c(4, NA, 6)), "結果"), "依 id 接起來:沒有出席的那一波是空的。" ) # "Wave 1", "Wave 2", "Result"; # caption: "Joined by id: a wave the child missed is empty."kit_diagram( list(kit_dg_table(data.frame(id = 1:2, t1 = c(3, 5)), "第 1 波"), kit_dg_table(data.frame(id = c(1, 3), t2 = c(4, 6)), "第 2 波")), kit_dg_table(data.frame(id = 1:3, t1 = c(3, 5, NA), t2 = c(4, NA, 6)), "結果"), "依 id 接起來:沒有出席的那一波是空的。" ) # "Wave 1", "Wave 2", "Result"; # caption: "Joined by id: a wave the child missed is empty."
Dimension text: "5 列 × 7 欄" ("5 rows × 7 columns")
kit_dims(nrow, ncol, preview_cols = NULL)kit_dims(nrow, ncol, preview_cols = NULL)
nrow, ncol
|
Number of rows and columns. |
preview_cols |
Give this when the screen previews only the first few columns (if fewer than the column count, adds "以下只預覽前 N 欄" ("previewing only the first N columns below")). |
A string.
kit_dims(3418, 25) kit_dims(3418, 25, preview_cols = 20)kit_dims(3418, 25) kit_dims(3418, 25, preview_cols = 20)
Anything unusual must be seen: when there are warnings, the status cannot be a plain 【完成】
(done).
kit_done_status( message, warnings = character(0), notes = character(0), points = character(0) )kit_done_status( message, warnings = character(0), notes = character(0), points = character(0) )
message |
The conclusion on the first line (one sentence, with numbers and units): a string or tag. |
warnings |
Things to watch out for (a character vector, or a list of strings and tags). Listed first. |
notes |
Additional notes (small gray text). |
points |
Key results (listed after |
A kit_status object.
kit_done_status("串接完成:3,389 人。") # "Join done: 3,389 children." kit_done_status("串接完成:3,389 人。", warnings = "有 12 人的 ID 重複,只留第一筆。") # warning: "12 children have duplicate IDs; only the first record is kept."kit_done_status("串接完成:3,389 人。") # "Join done: 3,389 children." kit_done_status("串接完成:3,389 人。", warnings = "有 12 人的 ID 重複,只留第一筆。") # warning: "12 children have duplicate IDs; only the first record is kept."
Looks the same as shiny::downloadButton(), but Chinese file names save correctly in every
browser: the last part of the URL, the Content-Disposition header (filename and
filename*), and <a download> all carry the same file name.
Put it inside renderUI(): it is registered again on each redraw, the URL changes with it, and
you never get a stale file.
kit_download_button( session, id, label, filename, content, class = NULL, app = getOption("kitDesign.app", "kit") )kit_download_button( session, id, label, filename, content, class = NULL, app = getOption("kitDesign.app", "kit") )
session |
The module's session (the namespace is added to the id automatically). |
id |
The button id (without namespace). |
label |
The button text, e.g. "下載 CSV" ("Download CSV"). |
filename |
The download file name (a string; may contain Chinese and spaces). |
content |
|
class |
An extra class, e.g. |
app |
The app name: used in log messages; when the file name is all Chinese, the ASCII
fallback name for old browsers is its lowercase form ("第一波.csv" → "kitjoin.csv").
Defaults to |
The file is written only when the user clicks download (content(file), which can read
reactives). If writing fails: the error goes to the log ("kit_abort() are shown to the user as written; other errors do not expose internal messages.
The button is a <button>, not a link: the URL and the file name are in data-kit-href and
data-kit-download, and kit.js (loaded by use_kit()) fetches the file in the background and
saves it, so the page never leaves. Browser extensions that take over links on the page (some
"document viewer" add-ons do, and the download then fails with "Not Found") find nothing to take.
Content-Type follows the extension: .csv, .zip, and .html get their own MIME types;
everything else (.sav, .csv.gz) is application/octet-stream, so browsers that add an
extension based on MIME type don't add an extra one.
<button type="button" class="btn btn-default kit-download" data-kit-href="網址" data-kit-download="檔名"> (data-kit-href is the download URL, data-kit-download the file
name).
kit_test_download() (for tests)
if (interactive()) { server <- function(input, output, session) { output$dl_ui <- shiny::renderUI(kit_download_button( session, "dl", "下載 CSV", "第一波 資料.csv", # "Download CSV", "Wave 1 data.csv" function(file) utils::write.csv(mtcars, file) )) } }if (interactive()) { server <- function(input, output, session) { output$dl_ui <- shiny::renderUI(kit_download_button( session, "dl", "下載 CSV", "第一波 資料.csv", # "Download CSV", "Wave 1 data.csv" function(file) utils::write.csv(mtcars, file) )) } }
For tables people search, page through, and click: search results, item lists to edit.
Same look as kit_static_table(): 2px frame, black header with white text, zebra stripes,
small type, numbers right-aligned; a selected row is light lime (bg-new), not blue. Text is
in Traditional Chinese ("共 37 列,第 1–20 列", "上一頁", "下一頁"); the page buttons look like
small buttons. Width fits the content; on a phone a wide table scrolls sideways inside its own
box, never the page.
kit_dt( df, selection = "none", page_length = 20L, search = FALSE, wrap_cols = NULL, min_widths = NULL, ordering = TRUE, options = list(), ... )kit_dt( df, selection = "none", page_length = 20L, search = FALSE, wrap_cols = NULL, min_widths = NULL, ordering = TRUE, options = list(), ... )
df |
A data.frame. Column names stay as they are (data and function output keep their English names). |
selection |
|
page_length |
Rows per page. 0 shows all rows (no paging). |
search |
Show the table's own search box. Usually |
wrap_cols |
Columns whose long text wraps (for example the item text); other columns do not wrap. |
min_widths |
Named vector of minimum column widths in CSS units, for example
|
ordering |
Click the header to sort. |
options |
More DataTables options (override the defaults). |
... |
Passed to |
Use it inside DT::renderDT(). 'DT' is in Suggests: the app itself needs Imports: DT.
For short result tables that need no paging, use kit_static_table().
A datatables htmlwidget with class kit-dt.
kit_static_table() for short result tables, kit_table_scroll().
if (requireNamespace("DT", quietly = TRUE)) { kit_dt(data.frame(itemID = c("a1", "a2"), label = c("睡眠時間", "午睡")), selection = "single", wrap_cols = "label") }if (requireNamespace("DT", quietly = TRUE)) { kit_dt(data.frame(itemID = c("a1", "a2"), label = c("睡眠時間", "午睡")), selection = "single", wrap_cols = "label") }
Plain-language errors from kit_abort() are shown directly; for unexpected errors it says what to
do, and the original message is tucked inside "原始訊息" ("Original message").
kit_error_status(e, action, todo = NULL)kit_error_status(e, action, todo = NULL)
e |
The error (a condition). |
action |
The action name, e.g. "串接" ("join"). |
todo |
A button or link for what to do (a tag); |
A kit_status object.
e <- tryCatch(stop("object 'x' not found"), error = identity) kit_error_status(e, "串接") u <- tryCatch(kit_abort("至少要 2 個波次的檔案。"), error = identity) # "Need files from at least 2 waves." kit_error_status(u, "串接")$messagee <- tryCatch(stop("object 'x' not found"), error = identity) kit_error_status(e, "串接") u <- tryCatch(kit_abort("至少要 2 個波次的檔案。"), error = identity) # "Need files from at least 2 waves." kit_error_status(u, "串接")$message
kit_fix_utf8_chr(): converts text to UTF-8 (characters that cannot be converted become ?);
returns non-text input as is.
kit_fix_utf8_df(): converts every text column (and factor) of a data frame to UTF-8, keeping
SAV variable labels and value labels.
kit_is_valid_utf8_df(): whether the column names and text columns are all valid UTF-8.
kit_sav_safe_names(): converts to valid SPSS variable names (tested against
haven::write_sav(): only letters, digits, and _ . @ # $; starts with a letter or @; does
not end with .; not a reserved word; at most 64 bytes; unique ignoring case, with _2, _3
added from the second duplicate on).
kit_truncate_bytes(): truncates by UTF-8 byte count without cutting a Chinese character in
half.
kit_gz_path(): readr decides whether to decompress from the extension; if the path does not end
in .gz, it copies the file to a temp file ending in .gz.
kit_open_browser(): opens the URL in the default browser (on Windows, unaffected by the
RStudio viewer).
kit_fix_utf8_chr(x) kit_fix_utf8_df(df) kit_is_valid_utf8_df(df) kit_sav_safe_names(cols) kit_truncate_bytes(x, max_bytes) kit_gz_path(path) kit_open_browser(url)kit_fix_utf8_chr(x) kit_fix_utf8_df(df) kit_is_valid_utf8_df(df) kit_sav_safe_names(cols) kit_truncate_bytes(x, max_bytes) kit_gz_path(path) kit_open_browser(url)
x |
A character vector ( |
df |
A data frame. |
cols |
Column names. |
max_bytes |
Maximum number of bytes. |
path |
A file path. |
url |
A URL. |
An object of the same kind as the input; kit_is_valid_utf8_df() gives TRUE / FALSE;
kit_open_browser() gives NULL.
kit_sav_safe_names(c("1st wave", "AND", "id", "ID")) kit_truncate_bytes(strrep("測", 30), 64)kit_sav_safe_names(c("1st wave", "AND", "id", "ID")) kit_truncate_bytes(strrep("測", 30), 64)
A regular shiny::tabsetPanel() with circled numbers in the tab names (① 資料, ② 題目校準…;
"Data", "Item calibration"). Tabs that are not a workflow step (e.g. "使用手冊", "User manual") go
in aside: dashed border, no shadow, pushed to the far right. Done tabs get a ✓ (an extra
.kit-tab-mark in the tab); tabs that can't be used yet are gray (kit_send_tab_states()).
kit_flow_tabs(..., id = "tabs", selected = NULL, aside = NULL)kit_flow_tabs(..., id = "tabs", selected = NULL, aside = NULL)
... |
|
id |
The tabsetPanel id ( |
selected |
The tab to start on (value). |
aside |
Values of tabs that are not a workflow step (a character vector). |
<div class="kit-flow-tabs"> containing a tabsetPanel().
kit_flow_tabs( shiny::tabPanel("① 資料", value = "data"), shiny::tabPanel("② 題目校準", value = "items"), shiny::tabPanel("使用手冊", value = "manual"), id = "flow", aside = "manual" )kit_flow_tabs( shiny::tabPanel("① 資料", value = "data"), shiny::tabPanel("② 題目校準", value = "items"), shiny::tabPanel("使用手冊", value = "manual"), id = "flow", aside = "manual" )
One app holding every component, filled with KIT examples.
The source is in system.file("examples/gallery", package = "kitDesign").
kit_gallery(..., brand = NULL)kit_gallery(..., brand = NULL)
... |
Arguments passed to |
brand |
Which brand to view: |
The result of shiny::runApp() (when the app closes).
if (interactive()) kit_gallery() if (interactive()) kit_gallery(brand = "demo")if (interactive()) kit_gallery() if (interactive()) kit_gallery(brand = "demo")
A lime color block, black border, header shadow; the full logo on the left, and on the right the
tool name, one subtitle line, and one version line. One per app, at the very top (kit_page()
already places it). It also sets the browser tab icon (the bean logo for KIT).
kit_header( title, subtitle = NULL, tagline = NULL, version = NULL, date = NULL, meta_extra = NULL, logo = TRUE, show_kitdesign = FALSE, brand = kit_current_brand(), package = NULL )kit_header( title, subtitle = NULL, tagline = NULL, version = NULL, date = NULL, meta_extra = NULL, logo = TRUE, show_kitdesign = FALSE, brand = kit_current_brand(), package = NULL )
title |
The tool name (the header's h1, also the browser tab title), for example "KIT 資料串接工具" ("KIT data join tool"). |
subtitle |
The subtitle (bold), for example the database name "臺灣幼兒發展調查資料庫" (the KIT database). |
tagline |
A phrase after the subtitle (for example "跨波次資料處理", "cross-wave data
processing"); |
version, date
|
The version and date on the header's version line, usually read from the
app's own DESCRIPTION: |
meta_extra |
Text added after the version line (for example "kits 0.25.0"). |
logo |
Whether the header shows the brand logo (KIT's is the bean plus the KIT wordmark):
|
show_kitdesign |
Whether to add the kitDesign version after the version line ("kitDesign 0.1.0"), so both versions are visible when someone reports a problem. |
brand |
The brand: logo, tab icon, and colors ( |
package |
The app's package name, written at the start of the version line:
"kitJoin v0.6.0 · 2026-10-31". Anyone reporting a problem can see at once which app and which
version. |
Logo, tab icon, and colors come from the brand (kit_brand()). When the brand's colors differ
from KIT's, the header adds a short <style> that overrides the differing --nb-* variables;
for the KIT brand nothing is added and the page is the same as before.
<div class="kit-header mb-4">.
kit_header("KIT 資料串接工具", "臺灣幼兒發展調查資料庫", tagline = "跨波次資料處理", version = "0.6.0", date = "2026-10-31", package = "kitJoin")kit_header("KIT 資料串接工具", "臺灣幼兒發展調查資料庫", tagline = "跨波次資料處理", version = "0.6.0", date = "2026-10-31", package = "kitJoin")
Checks the environment variable KIT_DEPLOY first ("cloud" or "local", set in Connect Cloud);
if it is not set, Posit Connect's default R_CONFIG_ACTIVE = "rsconnect" counts as the cloud
version.
kit_is_cloud()kit_is_cloud()
TRUE or FALSE.
kit_cloud_banner(), kit_cloud_footer()
kit_is_cloud()kit_is_cloud()
kit_abort()?Was the error thrown by kit_abort()?
kit_is_user_error(e)kit_is_user_error(e)
e |
The error (a condition). |
TRUE or FALSE.
kit_is_user_error(tryCatch(kit_abort("x"), error = identity))kit_is_user_error(tryCatch(kit_abort("x"), error = identity))
htmlDependency for the logo files (inst/logo/): the header SVG and tab icon are fetched by URL
kit_logo_dependency()kit_logo_dependency()
An htmltools::htmlDependency() object; file URLs are
kitDesign-logo-<version>/<file name> (kit_logo_file()).
kit_logo_dependency()$namekit_logo_dependency()$name
Used for both the header's vector logo and the tab icon; the URL is served by
kit_logo_dependency() (the page must include this dependency: kit_header() already attaches
it). To embed the logo in the page without a separate file, use kit_logo_uri() (a PNG data
URI).
kit_logo_file(file = "kit-logo.svg")kit_logo_file(file = "kit-logo.svg")
file |
A file name in |
A relative URL, e.g. "kitDesign-logo-0.1.0/kit-logo.svg".
kit_logo_file("kit-logo.svg")kit_logo_file("kit-logo.svg")
Goes straight into the page (no static file path to set up). The logos are transparent PNGs in
inst/logo/.
kit_logo_uri(size = 96, kind = c("mark", "logo"))kit_logo_uri(size = 96, kind = c("mark", "logo"))
size |
Pixels: the bean ( |
kind |
|
"data:image/png;base64,..."; "" when that size does not exist.
substr(kit_logo_uri(64), 1, 30)substr(kit_logo_uri(64), 1, 30)
"還差" ("Still needed"): the sentence shown when there are not enough files
kit_need_files(n_files, need = 2L)kit_need_files(n_files, need = 2L)
n_files |
How many files have been added. |
need |
The minimum number of files this page needs. |
One sentence; NULL when there are enough files.
kit_need_files(1L, need = 2L) kit_need_files(0L, need = 1L)kit_need_files(1L, need = 2L) kit_need_files(0L, need = 1L)
Creates a ready-to-run app package skeleton, with one example tab written to the KIT design language's page skeleton:
kit_new_app( path, title, subtitle = "臺灣幼兒發展調查資料庫", package = basename(path) )kit_new_app( path, title, subtitle = "臺灣幼兒發展調查資料庫", package = basename(path) )
path |
The new folder (the last part is the package name, e.g. |
title |
The tool name, e.g. "KIT 計分工具" ("KIT scoring tool"). |
subtitle |
The subtitle, e.g. "臺灣幼兒發展調查資料庫" (the KIT database). |
package |
The package name (defaults to the folder name): starts with a letter and contains only letters, digits, and periods. |
DESCRIPTION: Imports: kitDesign (>= current version),
Remotes: jiewenTsai/kitDesign@v<current version>.
app.R: the entry point for Posit Connect Cloud (primary file).
R/app.R: kit_page() plus the step tabs module (kit_step_tabs_ui()); run_app().
R/page_calc.R: an example tab using a primary button plus the ready line
(kit_run_gate_ui()), a status box, a result table, "How to do it in R"
(kit_code_panel_ui()), and next step.
tests/testthat/: one shiny::testServer() test.
.gitignore (including tests/testthat/_problems/ and testthat-problems.rds),
.Rbuildignore, README.md, LICENSE.
If the folder already exists and is not empty, it does nothing and stops (it never overwrites any file).
path (full path), invisibly.
dir <- file.path(tempdir(), "kitDemo") kit_new_app(dir, "KIT 示範工具", "臺灣幼兒發展調查資料庫") # "KIT demo tool", the KIT database list.files(dir, recursive = TRUE) unlink(dir, recursive = TRUE)dir <- file.path(tempdir(), "kitDemo") kit_new_app(dir, "KIT 示範工具", "臺灣幼兒發展調查資料庫") # "KIT demo tool", the KIT database list.files(dir, recursive = TRUE) unlink(dir, recursive = TRUE)
At the bottom of the page, aligned right: a line of small text on the left (it can be a list of places to go next), and a button on the right, "下一步:××× →" ("Next step: ××× →"). Shown only after a result is ready (【完成】 done, or 【完成,但請注意】 done, but take note); removed when the result is out of date (【要重新執行】, needs a rerun).
kit_next_step( text = NULL, button_id = NULL, label = NULL, button = NULL, kind = "primary" )kit_next_step( text = NULL, button_id = NULL, label = NULL, button = NULL, kind = "primary" )
text |
Small text on the left; |
button_id |
The button id; when |
label |
The button text, e.g. "下一步:橫向資料串接" ("Next step: wide join"); " →" is added automatically. |
button |
A button to use directly instead of an actionButton (e.g. a link to another app). |
kind |
The button kind: primary button by default; use |
<div class="kit-next">.
kit_next_step("串接好了,可以接著做 kits 作答檔。", "next", "下一步:kits 作答檔") # "Join done; you can go on to the kits response file." / "Next step: kits response file"kit_next_step("串接好了,可以接著做 kits 作答檔。", "next", "下一步:kits 作答檔") # "Join done; you can go on to the kits response file." / "Next step: kits response file"
【停下來】 (stopped) is red and shows for 10 seconds; everything else uses the regular style and shows for 6 seconds.
kit_notify(st, session = shiny::getDefaultReactiveDomain(), duration = NULL)kit_notify(st, session = shiny::getDefaultReactiveDomain(), duration = NULL)
st |
A |
session |
The Shiny session (defaults to the current session). |
duration |
How many seconds to show it; when |
The notification id (invisible); NULL for "idle" or when there is no session.
if (interactive()) { kit_notify(kit_done_status("串接完成。")) # "Join done." }if (interactive()) { kit_notify(kit_done_status("串接完成。")) # "Join done." }
KIT design language: numbers with four or more digits get thousands separators, as in "3,389 人" ("3,389 people"). Decimals are rounded to integers.
kit_num(x)kit_num(x)
x |
A number (or a string that can be converted to a number). |
A string, for example "3,418".
kit_num(3418) kit_num(c(5, 12345, 1e6))kit_num(3418) kit_num(c(5, 12345, 1e6))
Open a collapsible section (for example, open the advanced settings after applying a preset)
kit_open_details(id, session = shiny::getDefaultReactiveDomain())kit_open_details(id, session = shiny::getDefaultReactiveDomain())
id |
The id of the |
session |
Defaults to the current session. |
The full id, invisibly; NULL when there is no session.
if (interactive()) kit_open_details("advanced")if (interactive()) kit_open_details("advanced")
Wraps
bslib::page_fluid(lang = "zh-Hant-TW", theme = kit_theme(), use_kit(), kit_header(...), ...).
Below the header, put the tab bar (kit_navgroups() or kit_flow_tabs()) and the content of
each tab.
kit_page( title, subtitle = NULL, ..., tagline = NULL, version = NULL, date = NULL, logo = TRUE, meta_extra = NULL, window_title = title, show_kitdesign = FALSE, brand = kit_current_brand(), package = NULL )kit_page( title, subtitle = NULL, ..., tagline = NULL, version = NULL, date = NULL, logo = TRUE, meta_extra = NULL, window_title = title, show_kitdesign = FALSE, brand = kit_current_brand(), package = NULL )
title |
The tool name (the header's h1, also the browser tab title), for example "KIT 資料串接工具" ("KIT data join tool"). |
subtitle |
The subtitle (bold), for example the database name "臺灣幼兒發展調查資料庫" (the KIT database). |
... |
The page content. |
tagline |
A phrase after the subtitle (for example "跨波次資料處理", "cross-wave data
processing"); |
version, date
|
The version and date on the header's version line, usually read from the
app's own DESCRIPTION: |
logo |
Whether the header shows the brand logo (KIT's is the bean plus the KIT wordmark):
|
meta_extra |
Text added after the version line (for example "kits 0.25.0"). |
window_title |
The browser tab title (defaults to |
show_kitdesign |
Whether to add the kitDesign version after the version line ("kitDesign 0.1.0"), so both versions are visible when someone reports a problem. |
brand |
The brand: logo, tab icon, and colors ( |
package |
The app's package name, written at the start of the version line:
"kitJoin v0.6.0 · 2026-10-31". Anyone reporting a problem can see at once which app and which
version. |
A complete page (bslib::page_fluid()).
kit_header(), kit_app_description()
if (interactive()) { ui <- kit_page( "KIT ×× 工具", "臺灣幼兒發展調查資料庫", tagline = "跨波次資料處理", version = "0.1.0", date = "2026-10-31", kit_tab_guide("示範一頁。", kit_steps("選檔案", "按執行"), "一張表。") ) shiny::shinyApp(ui, function(input, output, session) {}) }if (interactive()) { ui <- kit_page( "KIT ×× 工具", "臺灣幼兒發展調查資料庫", tagline = "跨波次資料處理", version = "0.1.0", date = "2026-10-31", kit_tab_guide("示範一頁。", kit_steps("選檔案", "按執行"), "一張表。") ) shiny::shinyApp(ui, function(input, output, session) {}) }
Attaches an htmlDependency to a plotly chart that loads the rounded typeface Chiron GoRound TC
from Google Fonts (weights 400 and 700). Shiny pages already have this typeface (kit_theme()),
so this is mainly for single HTML files saved with htmlwidgets::saveWidget(): they also open
in the app's typeface; without a network connection they fall back to the system sans-serif.
kit_plotly_fonts(p)kit_plotly_fonts(p)
p |
A plotly chart (or another htmlwidget). |
The <link> carries data-external="1": when saveWidget(selfcontained = TRUE) bundles the
file with pandoc, the font files are not embedded in the HTML (a Chinese typeface is split into
hundreds of files, so embedding them would make the file very large).
kit_plotly_style() already calls it by default (fonts = TRUE); attaching it twice to the
same chart is the same as attaching it once.
The same chart, with one more htmlDependency (kit-plotly-fonts).
if (requireNamespace("plotly", quietly = TRUE)) { p <- kit_plotly_fonts(plotly::plot_ly(x = 1:3, y = c(2, 4, 3), type = "scatter", mode = "lines")) vapply(p$dependencies, function(d) d$name, "") }if (requireNamespace("plotly", quietly = TRUE)) { p <- kit_plotly_fonts(plotly::plot_ly(x = 1:3, y = c(2, 4, 3), type = "scatter", mode = "lines")) vapply(p$dependencies, function(d) d$name, "") }
Makes plotly charts look like part of the same app: typeface, text sizes, and colors all use KIT's design variables. Only the look changes, not the data (points, lines, axis ranges, tick positions, and hover text stay the same).
kit_plotly_style( p, legend = c("auto", "bottom", "right", "keep"), colorscale = TRUE, fonts = TRUE, filename = "kit-chart", font_size = 15, brand = kit_current_brand() )kit_plotly_style( p, legend = c("auto", "bottom", "right", "keep"), colorscale = TRUE, fonts = TRUE, filename = "kit-chart", font_size = 15, brand = kit_current_brand() )
p |
A plotly chart ( |
legend |
Where the legend goes:
|
colorscale |
When |
fonts |
When |
filename |
The file name (without extension) for the toolbar's "download PNG" and "download SVG". |
font_size |
Body text size (px); the other sizes are derived from it. |
brand |
The brand ( |
What changes:
Typeface: everything uses the app's rounded typeface "Chiron GoRound TC", sans-serif;
text is nb-ink (#141414), ticks are text-hint (#5a5a5a). Sizes: body font_size (default
15px), legend and hover box 1px smaller, ticks 2px smaller, title about h5 (1.2 times
font_size).
Title: left-aligned, bold. When the chart is too narrow, it wraps in the browser (breaking between Chinese characters or at spaces).
Every axis (xaxis, xaxis2…, yaxis…, including side-by-side small charts such as a
Wright map): axis line nb-ink 1.5px, grid lines line-soft (#e3ded2), zero line line
(#d9d4c5), ticks outside; no frame on four sides (mirror = FALSE, and the black frame kits
draws around each small chart is removed; KIT design language: no frames inside frames). When
axis titles and ticks do not fit, the margins widen automatically (automargin).
Background: white (the chart sits in a white card); downloaded PNGs and SVGs also have a white background.
Colors: colorway is Okabe–Ito (kit_chart_colors()). plotly's own default colors are
replaced with the Okabe–Ito color in the same position; colors set by kits or kitsUI stay
unchanged (kitsUI's role colors and category recoloring are kept).
Color scale (colorscale = TRUE): heatmap-type traces (heatmap, contour, histogram2d)
switch to kit_chart_sequential() (very light lime → light lime → KIT green); the color bar's
text and outline always get the new look.
Hover box: white background, nb-ink border, the app's typeface.
Legend: the app's typeface, no border. For position, see legend.
Toolbar: no plotly logo; only "download PNG", "download SVG", zoom in, zoom out, and reset
to the original range remain; interface text is Traditional Chinese (zh-TW). The PNG is
always 2100 pixels wide (7 inches at 300 dpi; the aspect ratio is the one on screen) and the
file says 300 dpi, so it is 7 inches wide when inserted in Word. The SVG is a vector image for
journals that want 600 dpi or more, EPS or PDF; its text uses the fonts on the computer that
opens it (the rounded typeface is a web font). Both are made in the browser, not by R, and
also work in a downloaded HTML file.
Margins: equally small on all sides; plotly widens them automatically when axes, legend, or color bar do not fit, so no text is cut off.
What does not change: data, axis ranges and ticks, trace colors (other than plotly's default
colors), marker shapes, line widths and dashes, opacity, hover text, legend text, the text of
titles and axis titles (only bold is added), chart height and width, and shapes (other than
kits' small-chart frames; for example, the gray band in fit plots).
Order when used with kits and kitsUI: kits' plot() → kitsUI recoloring
(kit_chart_categorical(), kit_chart_role()) → kitsUI Chinese labels (kit_chart_zh()) →
kit_plotly_style(). Recoloring and this function can be swapped (this function does not touch
set colors), but Chinese labels must come first: this function wraps the title in <b>,
after which kit_chart_zh() no longer recognizes the original English title.
Applying it twice to the same chart is the same as applying it once.
A plotly chart (passed through plotly_build()).
kit_plotly_fonts(), kit_chart_colors(), kit_chart_sequential()
if (requireNamespace("plotly", quietly = TRUE)) { d <- data.frame(wave = rep(c(3, 6, 12), 2), score = c(8, 11, 15, 7, 10, 13), cohort = rep(c("M03", "M36"), each = 3)) p <- plotly::plot_ly(d, x = ~wave, y = ~score, split = ~cohort, type = "scatter", mode = "lines+markers") p <- plotly::layout(p, title = "各世代的分數中位數") kit_plotly_style(p) }if (requireNamespace("plotly", quietly = TRUE)) { d <- data.frame(wave = rep(c(3, 6, 12), 2), score = c(8, 11, 15, 7, 10, 13), cohort = rep(c("M03", "M36"), each = 3)) p <- plotly::plot_ly(d, x = ~wave, y = ~score, split = ~cohort, type = "scatter", mode = "lines+markers") p <- plotly::layout(p, title = "各世代的分數中位數") kit_plotly_style(p) }
shiny::renderTable())With too many columns, only the first max_cols columns are kept (priority_cols are kept
first); only the first n rows are kept.
Each column is converted to easy-to-read text (value labels as "[value] label", dates as
yyyy-mm-dd). Put a kit_rows_note() line above it.
kit_preview_table( df, priority_cols = NULL, n = KIT_PREVIEW_ROWS, max_cols = KIT_PREVIEW_COLS )kit_preview_table( df, priority_cols = NULL, n = KIT_PREVIEW_ROWS, max_cols = KIT_PREVIEW_COLS )
df |
A data.frame. |
priority_cols |
Columns that must be in the preview (for example the ID column); they go first. |
n |
Maximum number of rows. |
max_cols |
Maximum number of columns. |
A data.frame (all columns are text).
df <- as.data.frame(matrix(1:60, nrow = 2)) dim(kit_preview_table(df, priority_cols = "V30", n = 1))df <- as.data.frame(matrix(1:60, nrow = 2)) dim(kit_preview_table(df, priority_cols = "V30", n = 1))
A list of individual problems inside a step card, such as "還差:…" ("Still needed: …") or "這幾欄有問題" ("these columns have problems").
kit_problems(..., head = NULL, bullets = TRUE)kit_problems(..., head = NULL, bullets = TRUE)
... |
One string (or tag) per problem. |
head |
Bold text in front, for example "還差:" ("Still needed:"); |
bullets |
|
<div class="kit-problems">; NULL when there are no problems.
kit_problems("第 2 列沒有選世代", "第 4 列的波次不是數字", head = "還差:") kit_problems("bsrs1:w1d5_1 和 w2d5_1 的分數範圍不同", bullets = FALSE)kit_problems("第 2 列沒有選世代", "第 4 列的波次不是數字", head = "還差:") kit_problems("bsrs1:w1d5_1 和 w2d5_1 的分數範圍不同", bullets = FALSE)
When the primary button cannot be pressed yet, a bold line above it says "還差:…" ("Still needed: …"), followed by a link to where to fix it. Write what is still needed as concrete tasks and numbers: "至少要 2 個波次的檔案,目前只有 1 個" ("Need files from at least 2 waves; there is only 1 now").
kit_ready_line(missing, link = NULL)kit_ready_line(missing, link = NULL)
missing |
What is still needed (character vector; |
link |
A link that follows (for example "到「檔案上傳」加入檔案 →", "Go to 'File upload'
to add files →"); |
<p class="kit-ready todo">; NULL when nothing is missing.
kit_run_gate(), kit_need_files()
kit_ready_line(c("至少要 2 個波次的檔案,目前只有 1 個", "選 ID 欄"))kit_ready_line(c("至少要 2 個波次的檔案,目前只有 1 個", "選 ID 欄"))
kit_code_panel()
output[["code"]] <- kit_render_code_panel(function() code_text()).
Nothing is computed while the panel is collapsed (Shiny does not render hidden outputs; it
computes only when "用 R 怎麼做" ("How to do it in R") is opened). When code() stops
(req()), fails, or is empty, only the empty line is shown.
kit_render_code_panel( code, note = KIT_CODE_NOTE, width = KIT_CODE_WIDTH, empty = KIT_CODE_EMPTY )kit_render_code_panel( code, note = KIT_CODE_NOTE, width = KIT_CODE_WIDTH, empty = KIT_CODE_EMPTY )
code |
A function or reactive that returns the code: the |
note, width
|
See |
empty |
The one line shown while there is no code yet. |
if (interactive()) { server <- function(input, output, session) { output$code <- kit_render_code_panel(function() "x <- 1") } }if (interactive()) { server <- function(input, output, session) { output$code <- kit_render_code_panel(function() "x <- 1") } }
One line of small kit-hint text placed above the table.
kit_rows_note(nrow, ncol = NULL, shown_rows, shown_cols = NULL)kit_rows_note(nrow, ncol = NULL, shown_rows, shown_cols = NULL)
nrow, ncol
|
Total number of rows and columns (with |
shown_rows, shown_cols
|
How many rows and columns are shown on screen (with
|
A string.
kit_rows_note(157, shown_rows = 20) kit_rows_note(3418, 25, shown_rows = 10, shown_cols = 20) kit_rows_note(8, shown_rows = 10)kit_rows_note(157, shown_rows = 20) kit_rows_note(3418, 25, shown_rows = 10, shown_cols = 20) kit_rows_note(8, shown_rows = 10)
Put shiny::uiOutput(ns("ready")) on the page, directly followed by
kit_button(ns("run"), ..., "primary", disabled = TRUE) (disabled at first; enabled once the
server has checked). The primary button is shown from the start, so people know what they will
press at the end of this page.
kit_run_gate( session, output, missing, link = function() NULL, button_id = "run", output_id = "ready" )kit_run_gate( session, output, missing, link = function() NULL, button_id = "run", output_id = "ready" )
session |
The module's session. |
output |
The module's output. |
missing |
A reactive: what is still needed (character; length 0 = ready to run). |
link |
function(): the link after "還差" ("Still needed") (a tag or |
button_id |
The primary button's id (the id inside the module, without |
output_id |
The |
Still check again inside the button's observeEvent(): disabling is only on screen and must not
be the only safeguard.
NULL, invisibly; call it inside the server.
if (interactive()) { server <- function(input, output, session) { kit_run_gate(session, output, shiny::reactive(kit_need_files(1L, need = 2L))) } }if (interactive()) { server <- function(input, output, session) { kit_run_gate(session, output, shiny::reactive(kit_need_files(1L, need = 2L))) } }
The primary button is shown (disabled) from the start; it can be pressed only when nothing is
still needed (missing() is empty). Above it, a bold line says "還差:…" ("Still needed: …"),
followed by a link to where to fix it. The reactive returned by the server changes only when the
button is pressed while nothing is still needed (the server also blocks it, not just the
disabled button), so a plain observeEvent(run(), …) is enough.
kit_run_gate_ui(id, label, ...) kit_run_gate_server(id, missing, link = function() NULL)kit_run_gate_ui(id, label, ...) kit_run_gate_server(id, missing, link = function() NULL)
id |
The module id. |
label |
The primary button's text (start with a verb, for example "執行串接", "Run join"). |
... |
Other arguments passed to |
missing |
A reactive: what is still needed (character vector; length 0 = ready to run). |
link |
A function: the link after "還差" ("Still needed") (a tag or |
kit_run_gate_ui(): the slot for the "還差" line and the primary button.
kit_run_gate_server(): a reactive counting presses made while ready to run (0 at first).
kit_ready_line(), kit_run_gate() (low-level)
if (interactive()) { ui <- kit_page("KIT 示範", NULL, shiny::selectInput("id_col", "ID 欄", c("", "subj_id")), kit_run_gate_ui("run", "執行串接") ) server <- function(input, output, session) { run <- kit_run_gate_server("run", shiny::reactive(if (input$id_col == "") "選 ID 欄")) shiny::observeEvent(run(), message("執行"), ignoreInit = TRUE) } shiny::shinyApp(ui, server) }if (interactive()) { ui <- kit_page("KIT 示範", NULL, shiny::selectInput("id_col", "ID 欄", c("", "subj_id")), kit_run_gate_ui("run", "執行串接") ) server <- function(input, output, session) { run <- kit_run_gate_server("run", shiny::reactive(if (input$id_col == "") "選 ID 欄")) shiny::observeEvent(run(), message("執行"), ignoreInit = TRUE) } shiny::shinyApp(ui, server) }
Page skeleton for a tab with one primary button. kit_run_tab_ui() lays out the fixed parts
following the page skeleton; the tab's own settings go in .... kit_run_tab_server() is
called inside the tab's moduleServer() and handles the status flow:
kit_run_tab_ui( id, run_label, guide = NULL, ..., gate = TRUE, result = FALSE, downloads = TRUE, details = TRUE, code = TRUE, next_step = FALSE ) kit_run_tab_server( input, output, session, run_label, run, done = NULL, ready = NULL, ready_link = function() NULL, settings = NULL, stale_on = NULL, progress = NULL, error = function(e) kit_error_status(e, run_label), guard = NULL, confirm = list(), result_ui = NULL, details_ui = NULL, code = NULL, code_note = KIT_CODE_NOTE )kit_run_tab_ui( id, run_label, guide = NULL, ..., gate = TRUE, result = FALSE, downloads = TRUE, details = TRUE, code = TRUE, next_step = FALSE ) kit_run_tab_server( input, output, session, run_label, run, done = NULL, ready = NULL, ready_link = function() NULL, settings = NULL, stale_on = NULL, progress = NULL, error = function(e) kit_error_status(e, run_label), guard = NULL, confirm = list(), result_ui = NULL, details_ui = NULL, code = NULL, code_note = KIT_CODE_NOTE )
id |
The tab (module) id. |
run_label |
The primary button's text (start with a verb, for example "執行串接", "Run join"). |
guide |
The "what this page does" at the top ( |
... |
The tab's own settings (inputs, hints), placed above the settings summary. |
gate |
|
result, downloads, next_step
|
The key results below the status box, the download area, and
the next step at the very bottom: |
details |
Details row: |
code |
UI ( |
input, output, session
|
The input, output, and session of the tab's |
run |
The function that runs: returns the result (any object), or a |
done |
|
ready |
A reactive: what is still needed (character vector; length 0 = ready to run); with
|
ready_link |
A function: the link after "還差" (a tag or |
settings |
A function: the parts of the settings summary (character vector; |
stale_on |
A function: things whose change makes the result out of date (for example
|
progress |
The progress bar message ("正在串接資料…", "Joining data…"); with |
error |
|
guard |
A function: a check before running; when it returns a |
confirm |
Errors the user must confirm: a named list; names are condition classes, values
are |
result_ui |
|
details_ui |
|
code_note |
The one-line note at the top of "用 R 怎麼做". |
Primary button: disabled while something is still needed (ready() is not empty), with a
"還差:…" ("Still needed: …") line above it (kit_run_gate()).
Settings summary: the parts of settings() joined into one line (kit_settings_line()).
Pressing the primary button: clear the old result → guard() → run() inside a progress bar
→ save the result, done(result) is the status; on error, error(e) is the status → status
box, notification (kit_notify()), scroll to the status box (kit_scroll_to()).
When settings or data change (stale_on() changes) and there is a result: the result is
cleared and the status is 【要重新執行】 ("Run again").
Key results, the first cell of the details row, and "How to do it in R": rendered only when there is a result.
Fixed output ids (without namespace): settings_line, ready, run (the button), status,
result, downloads, result_details, code, next_step.
kit_run_tab_ui(): a tagList().
kit_run_tab_server(): a list,
result: a reactiveVal, the result of this run (NULL when there is none yet or it is
out of date);
status: a reactiveVal, the status box's kit_status();
state: a reactive, the status box's state (used for the ✓ on step tabs,
kit_tab_states());
run(...): runs once, just like pressing the primary button (... goes to run(); for
example "仍要執行", "Run anyway", in a confirmation dialog).
kit_run_gate(), kit_status(), kit_code_panel()
if (interactive()) { mod_stack_ui <- function(id) { kit_run_tab_ui( id, "執行疊加", guide = kit_tab_guide("把各波資料上下疊起來。", c("按「執行疊加」", "看狀態框與預覽"), "一張長格式的資料。"), shiny::numericInput(shiny::NS(id, "n"), "幾個波次", 2), downloads = FALSE ) } mod_stack_server <- function(id) { shiny::moduleServer(id, function(input, output, session) { tab <- kit_run_tab_server( input, output, session, "執行疊加", ready = shiny::reactive(if (input$n < 2) "至少 2 個波次"), settings = function() paste0(input$n, " 個波次"), stale_on = function() input$n, progress = "正在疊加資料…", run = function() data.frame(wave = seq_len(input$n)), done = function(res) kit_done_status(paste0("疊加完成:", nrow(res), " 列。")), details_ui = function(res) kit_detail("結果預覽", kit_static_table(res)), code = function() "stacked <- dplyr::bind_rows(t1, t2, .id = \"wave\")" ) list(state = tab$state) }) } shiny::shinyApp(kit_page("KIT 示範", NULL, mod_stack_ui("stack")), function(input, output, session) mod_stack_server("stack")) }if (interactive()) { mod_stack_ui <- function(id) { kit_run_tab_ui( id, "執行疊加", guide = kit_tab_guide("把各波資料上下疊起來。", c("按「執行疊加」", "看狀態框與預覽"), "一張長格式的資料。"), shiny::numericInput(shiny::NS(id, "n"), "幾個波次", 2), downloads = FALSE ) } mod_stack_server <- function(id) { shiny::moduleServer(id, function(input, output, session) { tab <- kit_run_tab_server( input, output, session, "執行疊加", ready = shiny::reactive(if (input$n < 2) "至少 2 個波次"), settings = function() paste0(input$n, " 個波次"), stale_on = function() input$n, progress = "正在疊加資料…", run = function() data.frame(wave = seq_len(input$n)), done = function(res) kit_done_status(paste0("疊加完成:", nrow(res), " 列。")), details_ui = function(res) kit_detail("結果預覽", kit_static_table(res)), code = function() "stacked <- dplyr::bind_rows(t1, t2, .id = \"wave\")" ) list(state = tab$state) }) } shiny::shinyApp(kit_page("KIT 示範", NULL, mod_stack_ui("stack")), function(input, output, session) mod_stack_server("stack")) }
For a choice that every tab of the app depends on, such as which item bank is in use. It sits right under the tab bar, without a frame: a radio group (or a select), one line of small hint text (what is in use now, how big it is), small buttons on the right, and an optional area for what must be confirmed after choosing (a checklist and buttons). A thin line separates it from the page below.
kit_scope_bar(input, line = NULL, actions = NULL, panel = NULL)kit_scope_bar(input, line = NULL, actions = NULL, panel = NULL)
input |
The input: usually |
line |
Tags or text for the hint line, for example |
actions |
Small buttons on the right of the hint line ( |
panel |
Optional area under the line for what must be confirmed after choosing (for
example a |
Markup: div.kit-scopebar > the input, div.kit-scopebar-line (line, then
div.kit-scopebar-actions with actions), div.kit-scopebar-panel (panel). Use
span.kit-scopebar-todo in line for something still to do ("還沒選題庫").
<div class="kit-scopebar">.
kit_scope_bar( shiny::radioButtons("bank", "題庫", c("TDCYP" = "tdcyp", "上傳自己的" = "own"), inline = TRUE), line = "目前:TDCYP 題庫 · 412 題", actions = list(kit_button("bank_dl", "下載題庫"), kit_button("bank_new", "新增題庫")) )kit_scope_bar( shiny::radioButtons("bank", "題庫", c("TDCYP" = "tdcyp", "上傳自己的" = "own"), inline = TRUE), line = "目前:TDCYP 題庫 · 412 題", actions = list(kit_button("bank_dl", "下載題庫"), kit_button("bank_new", "新增題庫")) )
"Results first, details after": call kit_scroll_to("status") at the end of the primary
button's observeEvent(). It scrolls only after this round's outputs have all reached the
browser (session$onFlushed()), so the status box is already rendered.
When the user has set "reduce motion", it jumps there without smooth scrolling.
kit_scroll_to(id, session = shiny::getDefaultReactiveDomain())kit_scroll_to(id, session = shiny::getDefaultReactiveDomain())
id |
The output id inside the module (the module's namespace is added automatically), for
example |
session |
Defaults to the current session (called inside a module, that is the module's session). |
The full id, invisibly; NULL when there is no session.
if (interactive()) { shiny::observeEvent(input$run, { # …run… kit_scroll_to("status") }) }if (interactive()) { shiny::observeEvent(input$run, { # …run… kit_scroll_to("status") }) }
Works with both kinds of tab bar: grouped tab bars (kit_navgroups(), data-tabset is tabset)
and flow tabs (kit_flow_tabs(), id is tabset).
kit_send_tab_states(states, session = shiny::getDefaultReactiveDomain())kit_send_tab_states(states, session = shiny::getDefaultReactiveDomain())
states |
The result of |
session |
Defaults to the current session. |
states, invisibly.
if (interactive()) kit_send_tab_states(kit_tab_states(list(upload = "success")))if (interactive()) kit_send_tab_states(kit_tab_states(list(upload = "success")))
For example "本次:5 個檔案 · ID:各檔各自 · 全勤樣本" ("This run: 5 files · ID: per file · complete-attendance sample").
kit_settings_line(parts, prefix = "本次:")kit_settings_line(parts, prefix = "本次:")
parts |
The parts of the text (empty strings, |
prefix |
The text in front, default "本次:" ("This run:"); |
<p class="kit-settings-line">; NULL when there are no parts.
kit_settings_line(c("5 個檔案", "ID:各檔各自", "全勤樣本")) kit_settings_line("本回合 R1:M03、M36 · 參照世代 M03", prefix = NULL)kit_settings_line(c("5 個檔案", "ID:各檔各自", "全勤樣本")) kit_settings_line("本回合 R1:M03、M36 · 參照世代 M03", prefix = NULL)
Old result no longer valid after files or settings change (【要重新執行】, "Run again")
kit_stale_status(run_label, message = NULL)kit_stale_status(run_label, message = NULL)
run_label |
The primary button's text, for example "執行串接" ("Run join"). |
message |
Your own sentence (the default is "檔案或設定改過了,舊的結果已清除:請再按一次「…」。", "Files or settings changed and the old result was cleared: press '…' again."). |
A kit_status object.
kit_stale_status("執行串接")$messagekit_stale_status("執行串接")$message
White background, black header with white text, zebra stripes, width fits the content. Integer
columns get thousands separators automatically (3389 → 3,389).
Put an h4 title and a kit_rows_note() line above it.
kit_static_table(df, class = "table table-sm table-striped")kit_static_table(df, class = "table table-sm table-striped")
df |
A data.frame. |
class |
The table's class. |
<table class="table table-sm table-striped">.
kit_static_table(data.frame(世代 = c("M03", "M36"), 人數 = c(3389, 1250)))kit_static_table(data.frame(世代 = c("M03", "M36"), 人數 = c(3389, 1250)))
The result of an action. This is an S3 value object (class kit_status): its content is checked
when it is built. It has an htmltools::as.tags() method, so renderUI(st) or putting it straight
into tagList() renders it as a status box (the same as kit_status_ui()). print(st) prints a
text version.
kit_status( state = "idle", message = "", warnings = character(0), notes = character(0), details = NULL, action = NULL, points = character(0), details_title = "原始訊息" ) ## S3 method for class 'kit_status' as.tags(x, ...) ## S3 method for class 'kit_status' format(x, ...) ## S3 method for class 'kit_status' print(x, ...)kit_status( state = "idle", message = "", warnings = character(0), notes = character(0), details = NULL, action = NULL, points = character(0), details_title = "原始訊息" ) ## S3 method for class 'kit_status' as.tags(x, ...) ## S3 method for class 'kit_status' format(x, ...) ## S3 method for class 'kit_status' print(x, ...)
state |
|
message |
The conclusion on the first line (one sentence, with numbers and units): a string or tag. |
warnings |
Things to watch out for (a character vector, or a list of strings and tags). Listed first. |
notes |
Additional notes (small gray text). |
details |
The raw message (R's original error text; collapsed): a string or |
action |
A button or link for what to do (a tag); |
points |
Key results (listed after |
details_title |
The collapsible title for the raw message (kitsUI uses "kits 的原文", "kits' original text"). |
x |
A |
... |
Other content for |
Each of the four states has a fixed label and color:
| state | Label | Background |
"success" |
【完成】 ("Done") | mint green |
"warning" |
【完成,但請注意】 ("Done, but please note") | yellow |
"error" |
【停下來】 ("Stopped") | salmon |
"stale" |
【要重新執行】 ("Run again") | yellow |
"idle" (not run yet) is not shown. kitsUI's names are also accepted: "done", "warn", "stop".
A kit_status object.
kit_status_ui(), kit_done_status(), kit_error_status(), kit_stale_status()
st <- kit_status("success", "串接完成:結果共 3,389 列 × 2,058 欄。") st htmltools::as.tags(st)st <- kit_status("success", "串接完成:結果共 3,389 列 × 2,058 欄。") st htmltools::as.tags(st)
Renders a kit_status() as a status box: the first line is the label plus a one-sentence
conclusion; then up to 3 points (things to watch out for first, key results after), the rest
collapsed under "還有 N 點" ("N more points"); a button for what to do; small additional notes; the
collapsed raw message. After running, use kit_scroll_to() to scroll it to the top of the screen.
kit_status_ui(st, ..., max_points = KIT_STATUS_MAX_POINTS)kit_status_ui(st, ..., max_points = KIT_STATUS_MAX_POINTS)
st |
A |
... |
Other content placed last inside the box (kitsUI's |
max_points |
How many points to list directly (default 3). |
A kit_status object can also go straight into the page (its htmltools::as.tags() method is this
function); you only need to call this function to add more content or change the number of points.
<div class="alert alert-success kit-status" role="alert"> or similar; NULL when nothing
is shown.
kit_status_ui(kit_done_status( "串接完成:結果共 3,389 列 × 2,058 欄。", warnings = c("有 12 人的 ID 重複。", "第 3 波沒有 sex 欄。"), points = c("688 人每個檔案都有資料。", "第 1 波 3,389 人。") ))kit_status_ui(kit_done_status( "串接完成:結果共 3,389 列 × 2,058 欄。", warnings = c("有 12 人的 ID 重複。", "第 3 波沒有 sex 欄。"), points = c("688 人每個檔案都有資料。", "第 1 波 3,389 人。") ))
Tables inside the card have no outer border and a light header (CSS handles this); no indent on phones.
kit_step_body(...)kit_step_body(...)
... |
The content of this step; usually ends with "對,下一步" ("Yes, next step")
( |
<div class="kit-stepbody">.
kit_step_body(shiny::p("說明"), shiny::actionButton("ok1", "對,下一步"))kit_step_body(shiny::p("說明"), shiny::actionButton("ok1", "對,下一步"))
One card: on top is kit_step_head() (usually rendered by the server based on the state, in a
uiOutput()), below is the content of this step (kit_step_body(), usually wrapped in
conditionalPanel() so only the current step is open). The card's border and shadow follow the
header's state: the current step has a border and shadow; finished steps and steps not yet
available have no shadow and a gray border.
kit_step_card(head, ..., title_id = NULL)kit_step_card(head, ..., title_id = NULL)
head |
The header: |
... |
The card content ( |
title_id |
The id of the header title (the |
If you do not want to manage which step is open yourself, use the module kit_stepcards_ui() /
kit_stepcards_server().
<section class="kit-stepcard">.
kit_step_card( kit_step_head(1, "每個檔案是哪個世代、哪個波次", "current", title_id = "steptitle1"), kit_step_body(shiny::p("已經照欄名猜好了,請看一遍。"), shiny::actionButton("ok1", "對,下一步")), title_id = "steptitle1" )kit_step_card( kit_step_head(1, "每個檔案是哪個世代、哪個波次", "current", title_id = "steptitle1"), kit_step_body(shiny::p("已經照欄名猜好了,請看一遍。"), shiny::actionButton("ok1", "對,下一步")), title_id = "steptitle1" )
Three states:
kit_step_head( k, title, state = c("current", "done", "locked"), summary = NULL, edit_id = NULL, title_id = NULL )kit_step_head( k, title, state = c("current", "done", "locked"), summary = NULL, edit_id = NULL, title_id = NULL )
k |
The step number. |
title |
The title of this step. |
state |
|
summary |
A one-sentence summary shown when done (for example "5 個檔案 · 世代 TDCYP · 2009、2011(年)", "5 files · cohort TDCYP · 2009, 2011 (year)"). Not shown for the current step; a step not yet available always shows "先完成第 N−1 步" ("Finish step N−1 first"). |
edit_id |
The |
title_id |
The id of the title (pass it to the |
"current": the current step, a lime round number, aria-current="step".
"done": finished, a round ✓ (mint green), followed by a one-sentence summary, with "改"
("Edit") at the far right (when edit_id is given).
"locked": not yet available, gray, showing "先完成第 N−1 步" ("Finish step N−1 first").
Next to the number there is text for screen readers: "第 N 步,現在這一步:" ("Step N, current step:").
<div class="kit-stephead is-current"> or similar.
kit_step_head(1, "每個檔案是哪個世代、哪個波次", "done", summary = "5 個檔案 · 世代 TDCYP", edit_id = "edit1") kit_step_head(2, "哪些欄是題目", "locked")kit_step_head(1, "每個檔案是哪個世代、哪個波次", "done", summary = "5 個檔案 · 世代 TDCYP", edit_id = "edit1") kit_step_head(2, "哪些欄是題目", "locked")
Two looks, one set of states (done ✓; not yet available gray plus the reason; run again removes the ✓):
kit_step_tabs_ui(id, ..., groups = NULL, selected = NULL, aside = NULL) kit_step_tabs_server(id, states = NULL, tips = NULL)kit_step_tabs_ui(id, ..., groups = NULL, selected = NULL, aside = NULL) kit_step_tabs_server(id, states = NULL, tips = NULL)
id |
The module id. |
... |
|
groups |
Groups (a list of |
selected |
The tab selected at the start (value). |
aside |
Tabs in a flow that are not a step of the flow (value). |
states |
A reactive (or function): the state of each tab, a named character vector or list.
Values are |
tips |
The hover text for tabs not yet available: a named character vector (or a reactive that returns one), or one sentence for all. |
Ordered flow (kitsUI): do not pass groups; the tabPanel() names in ... use circled
numbers; aside tabs (for example "使用手冊", "User manual") are dashed and placed at the far
right.
Grouped, unnumbered (kitJoin): pass groups (a list of kit_navgroup()); the tab bar is
kit_navgroups(), and the real tabs are a hidden tabsetPanel().
The tabs' input is input[["<id>-tabs"]]; the selected() returned by the server is the same
value.
kit_step_tabs_ui(): the tab bar and the tabs.
kit_step_tabs_server(): a list,
selected: a reactive, the current tab (value);
select(value): switches to a tab;
states: a reactive, the states sent to the browser (the result of kit_tab_states()).
kit_flow_tabs(), kit_navgroups(), kit_tab_states() (low-level)
if (interactive()) { ui <- kit_page("KIT 示範", NULL, kit_step_tabs_ui("nav", shiny::tabPanel("① 資料", value = "data", "…"), shiny::tabPanel("② 題目校準", value = "items", "…") ) ) server <- function(input, output, session) { kit_step_tabs_server("nav", states = shiny::reactive(c(data = "success", items = "locked")), tips = c(items = "先在「① 資料」選好資料") ) } shiny::shinyApp(ui, server) }if (interactive()) { ui <- kit_page("KIT 示範", NULL, kit_step_tabs_ui("nav", shiny::tabPanel("① 資料", value = "data", "…"), shiny::tabPanel("② 題目校準", value = "items", "…") ) ) server <- function(input, output, session) { kit_step_tabs_server("nav", states = shiny::reactive(c(data = "success", items = "locked")), tips = c(items = "先在「① 資料」選好資料") ) } shiny::shinyApp(ui, server) }
Use when a page has several steps that must be done in order: only one step is open at a time; finished steps collapse to one line (✓, summary, "改" ("Edit")), and steps not yet available are gray. Each step ends with "對,下一步" ("Yes, next step") (a regular button); pressing it marks the step done and opens the next unfinished step. Press "改" on a finished step to go back.
kit_stepcards_ui(id, ..., next_label = "對,下一步") kit_stepcards_server( id, titles, summaries = list(), checks = list(), start = 1L )kit_stepcards_ui(id, ..., next_label = "對,下一步") kit_stepcards_server( id, titles, summaries = list(), checks = list(), start = 1L )
id |
The module id. |
... |
The content of each step (one tag or |
next_label |
The button text at the bottom of each step. |
titles |
The title of each step (character vector, length = number of steps). |
summaries |
The summary shown when each step is done: a list, one per step (a string,
function, or reactive; |
checks |
Checks before "對,下一步" ("Yes, next step") is pressed: a list, one function per
step, returning what is still needed (character vector; length 0 = can move on). While
something is still needed, the step is not done, and "還差:…" ("Still needed: …") is listed
above the button ( |
start |
Which step is open at the start. |
kit_stepcards_ui() holds the content of each step (in order); kit_stepcards_server() gives
each step's title, its summary when done, and the check before "對,下一步" is pressed.
kit_stepcards_ui(): a tagList() of kit_step_card()s.
kit_stepcards_server(): a list,
current: a reactive, which step is open now (0 when all are done);
done: a reactive, whether each step is done (logical vector);
open(k): opens step k (for example, back to step 1 when the data change);
reset(): marks all steps as not done and opens step 1.
kit_step_card(), kit_step_head() (low-level)
if (interactive()) { ui <- kit_page("KIT 示範", NULL, kit_stepcards_ui("steps", shiny::p("每個檔案是哪個世代、哪個波次。"), shiny::p("哪些欄是題目。"), shiny::p("計分檢查。") ) ) server <- function(input, output, session) { steps <- kit_stepcards_server("steps", titles = c("每個檔案是哪個世代、哪個波次", "哪些欄是題目", "計分檢查"), summaries = list("5 個檔案", function() "45 題", "沒有要決定的事") ) } shiny::shinyApp(ui, server) }if (interactive()) { ui <- kit_page("KIT 示範", NULL, kit_stepcards_ui("steps", shiny::p("每個檔案是哪個世代、哪個波次。"), shiny::p("哪些欄是題目。"), shiny::p("計分檢查。") ) ) server <- function(input, output, session) { steps <- kit_stepcards_server("steps", titles = c("每個檔案是哪個世代、哪個波次", "哪些欄是題目", "計分檢查"), summaries = list("5 個檔案", function() "45 題", "沒有要決定的事") ) } shiny::shinyApp(ui, server) }
steps of kit_tab_guide())Note: this is the <ol> inside the guide box, not a step card; step cards are
kit_step_card().
kit_steps(...)kit_steps(...)
... |
One string (or character vector) per step. |
<ol class="kit-steps">.
kit_steps("選檔案", "選 ID 欄", "按「執行串接」")kit_steps("選檔案", "選 ID 欄", "按「執行串接」")
A one-line guide at the top of each tab, collapsed by default: only the sentence "這一頁做什麼:" ("What this page does:") is visible, with "看步驟 ▸" ("See steps ▸") next to it; opening it shows the steps, what you get, additional notes, and technical notes. No border, small gray text, a light line below.
kit_tab_guide( goal, steps, output, technical = NULL, note = NULL, diagram = NULL )kit_tab_guide( goal, steps, output, technical = NULL, note = NULL, diagram = NULL )
goal |
One sentence, no more than 30 characters. |
steps |
The steps to follow: |
output |
What you get. |
technical |
Technical notes (for people who want to know how it works; collapsed one more
level); |
note |
Additional notes (small gray text); |
diagram |
A diagram ( |
Without a diagram, <details class="kit-guide kit-guide-page">; with a diagram, it is
wrapped in <div class="kit-guide kit-guide-wrap">.
kit_tab_guide( goal = "把多個波次的檔案依 ID 接成一張寬資料。", steps = kit_steps("選要串接的檔案", "選 ID 欄", "按「執行串接」"), output = "一張寬資料,可以下載 CSV、SAV。" )kit_tab_guide( goal = "把多個波次的檔案依 ID 接成一張寬資料。", steps = kit_steps("選要串接的檔案", "選 ID 欄", "按「執行串接」"), output = "一張寬資料,可以下載 CSV、SAV。" )
Works out how each tab looks from the state of its status box (the state of kit_status()):
kit_tab_states( status, locked = character(0), locked_tips = NULL, tabset = "tabs" )kit_tab_states( status, locked = character(0), locked_tips = NULL, tabset = "tabs" )
status |
A named list or character vector: names are tab values, values are states. Tabs not listed are left unchanged. |
locked |
Tabs that cannot be used yet: a character vector of values, or a named logical. |
locked_tips |
The hover text for tabs that cannot be used yet: one string (the same for all), or a character vector named by value. |
tabset |
The tabset id ( |
"locked" (gray): tabs listed in locked. The hover text is locked_tips.
"done" (with ✓): the state is "success" or "warning" (or directly "done").
"stale" (✓ removed): the state is "stale".
"ready": anything else ("idle", "error", "ready").
Pass the result to kit_send_tab_states() (or kit_watch_tab_states()) to send it to the
browser.
A list with one list(tabset, value, state, tip) per tab.
kit_tab_states( list(upload = "success", missing = "stale", join = "idle"), locked = "join", locked_tips = c(join = "還不能用:先加入至少 2 個檔案") )kit_tab_states( list(upload = "success", missing = "stale", join = "idle"), locked = "join", locked_tips = c(join = "還不能用:先加入至少 2 個檔案") )
Put wide tables in here: they scroll sideways in their own box and do not break the layout
kit_table_scroll(..., tall = FALSE)kit_table_scroll(..., tall = FALSE)
... |
Tables (for example |
tall |
For long tables: fixed height (about 26rem), with the header fixed at the top. |
<div class="kit-table-scroll">.
kit_table_scroll(kit_static_table(data.frame(id = 1:3)))kit_table_scroll(kit_static_table(data.frame(id = 1:3)))
The mock session of shiny::testServer() does not really register downloads
(registerDataObj() does nothing), so kit_download_button() stores the download in
session$userData instead; this function takes it from there and writes the file and responds
the same way as when a browser presses download.
kit_test_download(session, id, write = TRUE)kit_test_download(session, id, write = TRUE)
session |
The |
id |
The button id: inside a module, the id without namespace ( |
write |
When |
shiny::httpResponse() (status, content_type, headers, content$file), with one
more field filename: the file name registered. With write = FALSE, only
list(filename = ).
session <- shiny::MockShinySession$new() b <- kit_download_button(session, "dl", "下載", "a.csv", function(file) writeLines("x", file)) res <- kit_test_download(session, "dl") res$content_type readLines(res$content$file)session <- shiny::MockShinySession$new() b <- kit_download_button(session, "dl", "下載", "a.csv", function(file) writeLines("x", file)) res <- kit_test_download(session, "dl") res$content_type readLines(res$content$file)
Based on Bootswatch Brite (available from bslib 0.11.0), with KIT's colors; all text uses the
rounded typeface Chiron GoRound TC, code uses JetBrains Mono. Both typefaces are downloaded by
the browser from Google Fonts on demand (local = FALSE): CJK typefaces are split into hundreds
of unicode-range pieces, so the server does not fetch every font file at startup.
kit_theme(brand = kit_current_brand())kit_theme(brand = kit_current_brand())
brand |
The brand ( |
You usually do not need to call it yourself: kit_page() already uses it.
Colors come from the brand (kit_brand()): primary, success, navbar-bg, and link-color
are the brand's primary; link-hover-color is primary_hover; body-bg is page;
border-color is ink. info, warning, danger, and secondary are Bootstrap's own dark
colors and do not change with the brand (the background colors of callouts and status boxes are
in the CSS --nb-* variables, which do change with the brand).
brand.yml is not read (bslib::bs_theme(brand = FALSE)): when the working directory or a parent
has a _brand.yml (for example brand/_brand.yml), the theme does not change.
A bslib::bs_theme() object.
kit_page(), use_kit(), kit_brand()
theme <- kit_theme() bslib::theme_bootswatch(theme)theme <- kit_theme() bslib::theme_bootswatch(theme)
When there are results from this session (for example, after calibration), the browser asks once
before the tab is closed or reloaded.
Turn it on and off in the server based on the result:
shiny::observe(kit_unload_guard(!is.null(result()))).
kit_unload_guard(on = TRUE, session = shiny::getDefaultReactiveDomain())kit_unload_guard(on = TRUE, session = shiny::getDefaultReactiveDomain())
on |
|
session |
Defaults to the current session. |
on, invisibly.
if (interactive()) kit_unload_guard(TRUE)if (interactive()) kit_unload_guard(TRUE)
When reporting a problem, include both versions. The header's version line can include the
kitDesign version:
kit_page(..., show_kitdesign = TRUE) → "v0.5.0 · 2026-10-31 · kitDesign 0.1.0".
kit_versions(package = NULL)kit_versions(package = NULL)
package |
The app's package name (for example |
A named character vector, for example c(kitDesign = "0.1.0", kitJoin = "0.5.0").
kit_versions()kit_versions()
kit_watch_tab_states(session, shiny::reactive(kit_tab_states(...))).
kit_watch_tab_states(session, states)kit_watch_tab_states(session, states)
session |
The Shiny session. |
states |
A reactive: the result of |
The shiny::observe() observer (invisibly).
if (interactive()) { server <- function(input, output, session) { kit_watch_tab_states(session, shiny::reactive(kit_tab_states(list(upload = "success")))) } }if (interactive()) { server <- function(input, output, session) { kit_watch_tab_states(session, shiny::reactive(kit_tab_states(list(upload = "success")))) } }
Can go anywhere in the page (htmltools moves it into <head>). It contains:
use_kit()use_kit()
kit-base.css: basic styles for the header, guide boxes, callouts, and so on (formerly
kit_css(), kit_css_guide()).
kit-brite.css: KIT's neo-brutalism styles (formerly kit_css_brite()).
kit-design.css: the few rules kitDesign adds (header layout, dashed tabs in flow tabs, code
box width).
kit.js: collapsible sections, tab bars, tab states, scrolling to results, the reminder before
leaving, and the "複製" ("Copy") button.
kit_page() already adds it; you only need to add use_kit() when you build the page yourself
with bslib::page_*(), and pass theme = kit_theme() to the page.
An htmltools::htmlDependency() object.
use_kit()use_kit()