-
-
- Zero-provider-access setup
+
+
+
+
+
+
+ Privacy-first setup
+
+
+
+ Live YAML + CLI
+
+
+
+ Configure once. Run where you trust.
+
+
+ Tune the report, copy the generated workflow, and keep credentials inside GitHub
+ Actions or your local machine. Nothing on this page asks for your token.
+
-
- Build your Code Life Balance workflow
-
-
- Choose how you want the report to run. This page generates configuration only.
- Your GitHub token, private activity, and generated report do not need to pass through
- CodeLifeBalance infrastructure.
-
-
+
+
+ {[
+ ["Token", "Stays with you"],
+ ["Output", "SVG · JSON · MD"],
+ ["Setup", "No account"],
+ ].map(([label, value]) => (
+
+
+ {label}
+
+
{value}
+
+ ))}
+
+
From 83d2c079694290d4e4cc9d0be8b9140291246620 Mon Sep 17 00:00:00 2001
From: Ali HD <85040609+alihd-tech@users.noreply.github.com>
Date: Tue, 22 Sep 2026 00:13:13 +0330
Subject: [PATCH 2/2] Build compact responsive configurator workspace
---
components/privacy-configurator.tsx | 744 ++++++++++++++++++----------
1 file changed, 492 insertions(+), 252 deletions(-)
diff --git a/components/privacy-configurator.tsx b/components/privacy-configurator.tsx
index 9b334af..32e8020 100644
--- a/components/privacy-configurator.tsx
+++ b/components/privacy-configurator.tsx
@@ -1,14 +1,25 @@
"use client"
-import { useMemo, useState, type ComponentType, type ReactNode } from "react"
import {
+ useMemo,
+ useState,
+ type ComponentType,
+ type ReactNode,
+} from "react"
+import {
+ CalendarClock,
Check,
+ Clock3,
+ Code2,
Copy,
+ FileCode2,
+ FolderOutput,
GitBranch,
LockKeyhole,
+ Palette,
ShieldCheck,
- SlidersHorizontal,
Terminal,
+ UserRound,
Workflow,
} from "lucide-react"
import {
@@ -37,11 +48,14 @@ const timeZones = [
]
const fieldClass =
- "w-full rounded-lg border border-border bg-background px-3 py-2.5 text-sm text-foreground outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/15"
+ "h-9 w-full rounded-lg border border-border bg-background px-3 text-sm text-foreground outline-none transition placeholder:text-muted-foreground/65 focus:border-primary focus:ring-2 focus:ring-primary/15"
+
+type PreviewMode = "workflow" | "cli"
export function PrivacyConfigurator() {
const [config, setConfig] = useState
(defaultPrivacyConfig)
- const [copied, setCopied] = useState<"workflow" | "cli" | null>(null)
+ const [previewMode, setPreviewMode] = useState("workflow")
+ const [copied, setCopied] = useState(null)
const workflow = useMemo(() => generateWorkflow(config), [config])
const cli = useMemo(() => generateCliCommand(config), [config])
@@ -61,242 +75,377 @@ export function PrivacyConfigurator() {
})
}
- const copy = async (kind: "workflow" | "cli", value: string) => {
- await navigator.clipboard.writeText(value)
+ const copy = async (kind: PreviewMode, value: string) => {
+ try {
+ await navigator.clipboard.writeText(value)
+ } catch {
+ const textarea = document.createElement("textarea")
+ textarea.value = value
+ textarea.style.position = "fixed"
+ textarea.style.opacity = "0"
+ document.body.appendChild(textarea)
+ textarea.select()
+ document.execCommand("copy")
+ textarea.remove()
+ }
+
setCopied(kind)
window.setTimeout(() => setCopied(null), 1600)
}
+ const currentPreview = previewMode === "workflow" ? workflow : cli
+ const privateMode = config.includePrivate
+ const scheduleLabel =
+ config.schedule === "manual"
+ ? "Manual"
+ : config.schedule.charAt(0).toUpperCase() + config.schedule.slice(1)
+
return (
-
-
-
-
-
-
+
+
+
-
Configure your private workflow
-
- These settings are used only to generate YAML and a local CLI command in your browser.
+
Report settings
+
+ Changes update the generated setup instantly.
+
+ Local only
+
-
-
- update("username", event.target.value)}
- placeholder="octocat"
- className={fieldClass}
- />
-
-
-
- update("timeZone", event.target.value)}
- className={fieldClass}
- />
-
- {timeZones.map((zone) => (
-
- ))}
-
-
-
-
- {
- const start = Number(event.target.value)
- setConfig((current) => ({
- ...current,
- workdayStartHour: start,
- workdayEndHour:
- current.workdayEndHour <= start ? Math.min(24, start + 1) : current.workdayEndHour,
- }))
- }}
- className={fieldClass}
- >
- {Array.from({ length: 24 }, (_, hour) => (
-
- {String(hour).padStart(2, "0") + ":00"}
-
- ))}
-
-
-
-
- update("workdayEndHour", Number(event.target.value))}
- className={fieldClass}
- >
- {Array.from({ length: 24 }, (_, index) => index + 1)
- .filter((hour) => hour > config.workdayStartHour)
- .map((hour) => (
-
- {hour === 24 ? "24:00" : String(hour).padStart(2, "0") + ":00"}
-
- ))}
-
-
-
-
- update("theme", event.target.value as PrivacyConfig["theme"])}
- className={fieldClass}
- >
- Dark
- Light
-
-
-
-
-
- update("cardStyle", event.target.value as PrivacyConfig["cardStyle"])
- }
- className={fieldClass}
- >
- Detailed
- Compact
-
-
-
-
-
- update("schedule", event.target.value as PrivacyConfig["schedule"])
- }
- className={fieldClass}
- >
- Manual only
- Daily
- Weekly
- Monthly
- Custom cron
-
-
-
-
- update("outputDir", event.target.value)}
- className={fieldClass}
- />
-
+
+
+
+
+ update("username", event.target.value)}
+ placeholder="octocat"
+ className={fieldClass}
+ />
+
- {config.schedule === "custom" && (
-
-
+
update("customCron", event.target.value)}
+ list="code-life-timezones"
+ value={config.timeZone}
+ onChange={(event) => update("timeZone", event.target.value)}
className={fieldClass}
/>
+
+ {timeZones.map((zone) => (
+
+ ))}
+
- )}
-
-
Output formats
-
- {(["svg", "json", "markdown"] as ReportFormat[]).map((format) => {
- const active = config.formats.includes(format)
- return (
-
toggleFormat(format)}
- className={
- "rounded-lg border px-3 py-2.5 text-left text-sm font-medium transition " +
- (active
- ? "border-primary bg-primary/10 text-primary"
- : "border-border bg-background text-muted-foreground hover:text-foreground")
- }
- >
- {format === "svg" ? "SVG card" : format === "json" ? "JSON data" : "Markdown"}
-
- )
- })}
+
+
+ {
+ const start = Number(event.target.value)
+ setConfig((current) => ({
+ ...current,
+ workdayStartHour: start,
+ workdayEndHour:
+ current.workdayEndHour <= start
+ ? Math.min(24, start + 1)
+ : current.workdayEndHour,
+ }))
+ }}
+ className={fieldClass}
+ >
+ {Array.from({ length: 24 }, (_, hour) => (
+
+ {String(hour).padStart(2, "0")}:00
+
+ ))}
+
+
+
+
+ update("workdayEndHour", Number(event.target.value))}
+ className={fieldClass}
+ >
+ {Array.from({ length: 24 }, (_, index) => index + 1)
+ .filter((hour) => hour > config.workdayStartHour)
+ .map((hour) => (
+
+ {hour === 24 ? "24:00" : `${String(hour).padStart(2, "0")}:00`}
+
+ ))}
+
+
-
+
-
-
update("commit", value)}
- title="Commit generated files"
- description="Write the report back to this repository. Otherwise the workflow uploads it as a GitHub artifact."
- />
- update("includePrivate", value)}
- title="Include private repositories"
- description="Uses a user-owned CODE_LIFE_TOKEN secret. The secret remains in GitHub Actions and is never sent to CodeLifeBalance."
- sensitive
+
+
+ update("theme", value as PrivacyConfig["theme"])}
+ />
+
+ update("cardStyle", value as PrivacyConfig["cardStyle"])
+ }
+ />
+
+
+
+
+
+ update("outputDir", event.target.value)}
+ placeholder="code-life-balance"
+ className={fieldClass}
+ />
+
+
+
+
Output formats
+
+ {([
+ ["svg", "SVG", "Card"],
+ ["json", "JSON", "Data"],
+ ["markdown", "MD", "Report"],
+ ] as const).map(([format, shortLabel, caption]) => {
+ const active = config.formats.includes(format)
+ return (
+ toggleFormat(format)}
+ className={
+ "rounded-lg border px-2.5 py-2 text-left transition " +
+ (active
+ ? "border-primary/60 bg-primary/10 text-primary"
+ : "border-border bg-background text-muted-foreground hover:border-primary/30 hover:text-foreground")
+ }
+ >
+ {shortLabel}
+
+ {caption}
+
+
+ )
+ })}
+
+
+
+
+ update("commit", value)}
+ title="Commit generated files"
+ description="Otherwise upload the report as a GitHub artifact."
+ />
+ update("includePrivate", value)}
+ title="Include private repositories"
+ description="Uses your own CODE_LIFE_TOKEN secret inside GitHub Actions."
+ sensitive
+ />
+
+
+
+
+
+ update("schedule", value as PrivacyConfig["schedule"])
+ }
+ wrap
/>
-
+
+ {config.schedule === "custom" && (
+
+
+ update("customCron", event.target.value)}
+ placeholder="17 3 * * 1"
+ className={fieldClass}
+ />
+
+
+ )}
+
+
- {config.includePrivate && (
-
-
-
-
-
Private mode: create a fine-grained
- GitHub token with only the repositories and read permissions you want, then store it
- as the repository secret
CODE_LIFE_TOKEN.
+
+
+
+
+
+
Generated setup
+
+ Ready to paste into GitHub or run locally.
+
+
copy(previewMode, currentPreview)}
+ className="flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-border bg-background px-2.5 text-xs font-semibold transition hover:border-primary/40 hover:text-primary"
+ >
+ {copied === previewMode ? (
+
+ ) : (
+
+ )}
+ {copied === previewMode ? "Copied" : "Copy"}
+
+
+
+
+
setPreviewMode("workflow")}
+ />
+ setPreviewMode("cli")}
+ />
- )}
-
-
- copy("workflow", workflow)}
- />
+
+
+
+ (item === "markdown" ? "MD" : item.toUpperCase())).join(" · ")}
+ />
+
+
- copy("cli", cli)}
- singleLine
- />
+ {privateMode && (
+
+
+
+
+ Private mode expects a fine-grained GitHub token stored as{" "}
+ CODE_LIFE_TOKEN.
+
+
+
+ )}
+
+
+
-
-
+
-
-
-
+
+
+ )
+}
+
+function ConfigSection({
+ icon: Icon,
+ title,
+ description,
+ children,
+}: {
+ icon: ComponentType<{ className?: string }>
+ title: string
+ description: string
+ children: ReactNode
+}) {
+ return (
+
+
+
+
+
+
+
{title}
+
+ {description}
+
+
+
+ {children}
)
}
@@ -312,14 +461,64 @@ function Field({
}) {
return (
- {label}
+ {label}
{children}
- {hint && {hint} }
+ {hint && (
+
+ {hint}
+
+ )}
)
}
-function Toggle({
+function ChoiceGroup({
+ label,
+ value,
+ options,
+ onChange,
+ wrap = false,
+}: {
+ label: string
+ value: string
+ options: Array<{ value: string; label: string }>
+ onChange: (value: string) => void
+ wrap?: boolean
+}) {
+ return (
+
+
{label}
+
+ {options.map((option) => {
+ const active = option.value === value
+ return (
+ onChange(option.value)}
+ className={
+ "min-w-0 flex-1 rounded-md px-2.5 py-1.5 text-[11px] font-semibold transition " +
+ (active
+ ? "bg-primary text-primary-foreground shadow-sm"
+ : "text-muted-foreground hover:bg-muted hover:text-foreground")
+ }
+ >
+ {option.label}
+
+ )
+ })}
+
+
+ )
+}
+
+function CompactToggle({
checked,
onChange,
title,
@@ -335,31 +534,36 @@ function Toggle({
return (
onChange(!checked)}
className={
- "flex w-full items-start gap-3 rounded-xl border p-4 text-left transition " +
- (checked ? "border-primary/50 bg-primary/5" : "border-border bg-background")
+ "flex w-full items-center gap-3 rounded-lg border px-3 py-2.5 text-left transition " +
+ (checked
+ ? "border-primary/45 bg-primary/7"
+ : "border-border bg-background hover:border-primary/25")
}
>
-
-
+
+
+
{title}
- {sensitive && }
+ {sensitive && }
-
+
{description}
@@ -367,46 +571,80 @@ function Toggle({
)
}
-function CodePanel({
+function PreviewTab({
+ active,
icon: Icon,
- title,
- subtitle,
+ label,
+ onClick,
+}: {
+ active: boolean
+ icon: ComponentType<{ className?: string }>
+ label: string
+ onClick: () => void
+}) {
+ return (
+
+
+ {label}
+
+ )
+}
+
+function SummaryItem({
+ icon: Icon,
+ label,
value,
- copied,
- onCopy,
- singleLine = false,
}: {
icon: ComponentType<{ className?: string }>
- title: string
- subtitle: string
+ label: string
value: string
- copied: boolean
- onCopy: () => void
- singleLine?: boolean
}) {
return (
-
-
-
-
- {copied ? : }
- {copied ? "Copied" : "Copy"}
-
+
+
+
+ {label}
+
+
{value}
+
+ )
+}
+
+function CodePreview({
+ mode,
+ value,
+}: {
+ mode: PreviewMode
+ value: string
+}) {
+ return (
+
+
+
+ {mode === "workflow"
+ ? ".github/workflows/code-life-balance.yml"
+ : "terminal"}
+
+
+ {mode === "workflow" ? "YAML" : "Shell"}
+
{value}
@@ -415,7 +653,7 @@ function CodePanel({
)
}
-function TrustCard({
+function TrustNote({
icon: Icon,
title,
text,
@@ -425,10 +663,12 @@ function TrustCard({
text: string
}) {
return (
-
-
-
{title}
-
{text}
+
)
}