@growthub/cli 0.14.6 → 0.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +5 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +5 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +58 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +31 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/StatusPill.jsx +22 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ToggleField.jsx +5 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceDataModelCanvas.jsx +457 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +188 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +67 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-map/page.jsx +14 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +48 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-node-status.js +55 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-impact.js +198 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +11 -0
- package/package.json +1 -1
|
@@ -39,6 +39,27 @@ mcpTools: []
|
|
|
39
39
|
|
|
40
40
|
The **Workspace** is the top-level Growthub Local product object. This kit is the official starter that bootstraps every governed Workspace; everything else (worker kits, templates, workflows, hosted agents, source imports) is an input to a Workspace.
|
|
41
41
|
|
|
42
|
+
## Your operating role (read first)
|
|
43
|
+
|
|
44
|
+
**If you are an agent reading this, you are the operating layer.** A human gave you an outcome; you operate this governed Workspace on their behalf. When the requester is a super admin or workspace admin, you can act as the super-admin orchestrator inside the governed boundary; when the requester is a regular user, you still do the orchestration work, but approval, review, rollback, and authority stay with the workspace/super-admin surfaces. You are not editing files and hoping tests catch mistakes — you are operating a live domain model where the platform rejects invalid reality before it lands and remembers everything that does. The chat you are in *is* the user's steering surface; the no-code Workspace is the control and audit plane.
|
|
45
|
+
|
|
46
|
+
**The human is usually non-technical, and asks for a business outcome — not workspace mechanics.** They will not mention the Data Model, metadata graph, routes, validators, the PATCH boundary, or the run console — those are the control plane and proof layer that exist so you can operate safely and the super admin can inspect everything. They ask in plain language: *research a customer avatar, run market/competitor research, generate this week's content batch, script and brief the next campaign, pull the latest campaign performance, update the client report, build an MRR-by-plan dashboard, run the revenue workflow, iterate on a feature, find what broke.* Your job is to translate that outcome into governed operations over **what already exists** in this Workspace (prebuilt workflows, objects, and procedures), execute it, and deliver the result — outputs saved, synced, inspectable, and governed. This **private repository is the customer's durable operating environment**: their kit, objects, workflows, procedures, rules, source records, docs, receipts, run history, and governance boundaries all live here; your session is just the operator entering that universe.
|
|
47
|
+
|
|
48
|
+
The completion bar is the user's real-world outcome, not a partial proposal or a hidden local artifact. Continue the governed loop until the workspace has objective proof: the relevant workflow ran or object changed, output rows/ledgers reflect the count and state, durable storage or source records hold the deliverables, receipts/run ids explain what happened, and any human-review state remains explicit. Do not hard-code one client's workflow as the pattern; swap the client, rows, registries, brand constraints, storage prefix, dashboard, and quality criteria while keeping the same governed causation loop.
|
|
49
|
+
|
|
50
|
+
Your loop, every time:
|
|
51
|
+
|
|
52
|
+
1. **Inherit state** — read the `workspace:agent-outcomes` receipt stream (`GET /api/workspace/agent-outcomes`) and `.growthub-fork/project.md` to see what the last agent did; continue from `nextActions` / `rollbackRef`, don't redo work.
|
|
53
|
+
2. **Check what exists** — a scheduled job, external API, data view, or multi-agent workflow is almost always already a governed object. Prefer operating an existing object over writing code.
|
|
54
|
+
3. **Act only through governed routes** — `PATCH /api/workspace` (config) and `POST /api/workspace/sandbox-run` (execution); drafts via `workflow/publish`; proposals via `helper/apply`. There is no third path.
|
|
55
|
+
4. **Let the validator correct you** — preflight, read the rejection reason, repair, retry. Rejections are navigation, not failure.
|
|
56
|
+
5. **Persist the outcome** — count only connected, durable outputs; save accepted artifacts to the governed ledger/storage surface; keep generated binaries and secrets out of git.
|
|
57
|
+
6. **Leave proof** — every governed action emits a secret-redacted receipt. The human does not need the mechanics; the super admin inspects all of it after the fact (Workspace Map, Run Console, outcome cockpit).
|
|
58
|
+
|
|
59
|
+
**Three roles:** the human states outcomes → you (the agent) operate → the workspace admin/super admin governs and audits. The mechanics of the boundary are in [`skills/governed-workspace-mutation/SKILL.md`](./skills/governed-workspace-mutation/SKILL.md) — read it before any mutation.
|
|
60
|
+
|
|
61
|
+
> **For the human operator:** you do not have to operate this Workspace yourself. Tell an agent what you want; it operates the Workspace through governed routes; you (or your admin) inspect every change with full proof and rollback. The no-code Builder is the governed substrate and the audit surface — not a tool you must personally drive.
|
|
62
|
+
|
|
42
63
|
Every Growthub governed Workspace is materialised from this kit. The kit ships the `.growthub-fork/` contract (identity, policy, trace, optional authority), the `apps/workspace` no-code Workspace Builder, the validated `growthub.config.json` V1 contract, plus the six primitive layers Claude/Cursor/Codex agents operate against:
|
|
43
64
|
|
|
44
65
|
1. **`SKILL.md`** — this file. Discovery entry + routing menu. Always loaded first; the full operator runbook (`skills.md`) is disclosed progressively when work begins.
|
|
@@ -18,7 +18,7 @@ It intentionally depends on adapter contracts:
|
|
|
18
18
|
- `NANGO_ENVIRONMENT` (default `dev`)
|
|
19
19
|
- `NANGO_MODE` (`cloud` | `self-hosted`, default `cloud`)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
This `apps/workspace` app is the only bundled app surface; the legacy `studio/` Vite shell has been removed. It is the governed control plane and audit surface — non-technical users do not operate it directly; an agent operates the Workspace on their behalf through the governed routes (see the workspace `SKILL.md` operating-role contract), while super admins use this app for inspection, proof, and governance.
|
|
22
22
|
|
|
23
23
|
Settings exposes two universal integration lanes:
|
|
24
24
|
|
|
@@ -413,6 +413,11 @@ function buildRunResponse({
|
|
|
413
413
|
if (result && typeof result === "object" && Array.isArray(result.logTree)) {
|
|
414
414
|
base.logTree = result.logTree;
|
|
415
415
|
}
|
|
416
|
+
// Per-node orchestration execution trace (general pipeline, not swarm) so the
|
|
417
|
+
// Workflow Canvas can settle per-node status from the persisted record.
|
|
418
|
+
if (result && typeof result === "object" && Array.isArray(result.nodeTrace)) {
|
|
419
|
+
base.nodeTrace = result.nodeTrace;
|
|
420
|
+
}
|
|
416
421
|
base.exports = {
|
|
417
422
|
available: ["download-json", "copy-output", "download-stdout", "download-stderr", "download-log-node"],
|
|
418
423
|
external: []
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { useEffect, useMemo, useState } from "react";
|
|
22
22
|
import Link from "next/link";
|
|
23
|
-
import { Activity, BarChart3, Check, Copy, Eye, GitBranch, MoreVertical, Search } from "lucide-react";
|
|
23
|
+
import { Activity, BarChart3, Check, Copy, Eye, GitBranch, MoreVertical, Search, Share2 } from "lucide-react";
|
|
24
24
|
import { deriveWorkspaceState, deriveSwarmConditionPacket, deriveWorkspaceContributions, deriveLensWalkthroughState, LENS_WALKTHROUGH_DISMISS_FLAG } from "@/lib/workspace-activation";
|
|
25
25
|
import { WorkspaceContributionGraph } from "./WorkspaceContributionGraph.jsx";
|
|
26
26
|
import { WorkspaceLensWalkthrough } from "./WorkspaceLensWalkthrough.jsx";
|
|
@@ -244,9 +244,10 @@ export function WorkspaceLensPanel({ workspaceConfig, workspaceSourceRecords, me
|
|
|
244
244
|
<h1 className="workspace-lens-title">Workspace Lens</h1>
|
|
245
245
|
<p className="workspace-lens-subtitle">Live derived state for this workspace.</p>
|
|
246
246
|
</div>
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
247
|
+
<Link className="workspace-lens-map-link" href="/workspace-map" aria-label="Open Workspace Map">
|
|
248
|
+
<Share2 size={15} aria-hidden="true" />
|
|
249
|
+
<span>Workspace Map</span>
|
|
250
|
+
</Link>
|
|
250
251
|
</header>
|
|
251
252
|
|
|
252
253
|
{showWalk ? (
|
|
@@ -2390,6 +2390,7 @@ function DataModelTableSurface({
|
|
|
2390
2390
|
const [selectMenuOpen, setSelectMenuOpen] = useState(false);
|
|
2391
2391
|
const [pageSize, setPageSize] = useState(15);
|
|
2392
2392
|
const [pageIndex, setPageIndex] = useState(0);
|
|
2393
|
+
const [search, setSearch] = useState("");
|
|
2393
2394
|
const fieldInputRef = useRef(null);
|
|
2394
2395
|
const selectedOriginalIndex = selectedRecordIndex ?? localSelectedOriginalIndex;
|
|
2395
2396
|
|
|
@@ -2407,6 +2408,7 @@ function DataModelTableSurface({
|
|
|
2407
2408
|
setLastSelectedRowIndex(null);
|
|
2408
2409
|
setSelectMenuOpen(false);
|
|
2409
2410
|
setPageIndex(0);
|
|
2411
|
+
setSearch("");
|
|
2410
2412
|
}, [table.id]);
|
|
2411
2413
|
|
|
2412
2414
|
useEffect(() => {
|
|
@@ -2428,7 +2430,14 @@ function DataModelTableSurface({
|
|
|
2428
2430
|
const visibleColumns = useMemo(() => orderedColumns.filter((column) => !settings.hidden.includes(column)), [orderedColumns, settings.hidden]);
|
|
2429
2431
|
const rowEntries = useMemo(() => {
|
|
2430
2432
|
const indexed = (table.rows || []).map((row, originalIndex) => ({ row, originalIndex }));
|
|
2431
|
-
const
|
|
2433
|
+
const needle = search.trim().toLowerCase();
|
|
2434
|
+
const filtered = indexed.filter((entry) => {
|
|
2435
|
+
if (!rowMatchesFilter(entry.row, settings.filter)) return false;
|
|
2436
|
+
if (!needle) return true;
|
|
2437
|
+
// Quick search across the visible columns only — additive to the
|
|
2438
|
+
// column-level filter clauses, never replaces them.
|
|
2439
|
+
return visibleColumns.some((column) => String(entry.row?.[column] ?? "").toLowerCase().includes(needle));
|
|
2440
|
+
});
|
|
2432
2441
|
if (!settings.sort?.length) return filtered;
|
|
2433
2442
|
const clauses = settings.sort;
|
|
2434
2443
|
return [...filtered].sort((left, right) => {
|
|
@@ -2439,7 +2448,7 @@ function DataModelTableSurface({
|
|
|
2439
2448
|
}
|
|
2440
2449
|
return 0;
|
|
2441
2450
|
});
|
|
2442
|
-
}, [table.rows, settings]);
|
|
2451
|
+
}, [table.rows, settings, search, visibleColumns]);
|
|
2443
2452
|
const selectedRowCount = selectedRows.size;
|
|
2444
2453
|
const pageCount = Math.max(1, Math.ceil(rowEntries.length / pageSize));
|
|
2445
2454
|
const safePageIndex = Math.min(pageIndex, pageCount - 1);
|
|
@@ -2491,7 +2500,7 @@ function DataModelTableSurface({
|
|
|
2491
2500
|
setSelectedRow(null);
|
|
2492
2501
|
setLastSelectedRowIndex(null);
|
|
2493
2502
|
setSelectMenuOpen(false);
|
|
2494
|
-
}, [settings.filter, settings.sort, pageSize]);
|
|
2503
|
+
}, [settings.filter, settings.sort, pageSize, search]);
|
|
2495
2504
|
|
|
2496
2505
|
function commitField() {
|
|
2497
2506
|
const name = fieldName.trim();
|
|
@@ -2690,9 +2699,25 @@ function DataModelTableSurface({
|
|
|
2690
2699
|
))}
|
|
2691
2700
|
</div>
|
|
2692
2701
|
<div className="dm-records-actions">
|
|
2702
|
+
<label className="dm-toolbar-search">
|
|
2703
|
+
<Search size={13} aria-hidden="true" />
|
|
2704
|
+
<input
|
|
2705
|
+
value={search}
|
|
2706
|
+
placeholder="Search records"
|
|
2707
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
2708
|
+
aria-label={`Search ${table.label || "records"}`}
|
|
2709
|
+
/>
|
|
2710
|
+
</label>
|
|
2711
|
+
<span className="dm-toolbar-count">
|
|
2712
|
+
{(search.trim() || settings.filter?.clauses?.length)
|
|
2713
|
+
? `${rowEntries.length} of ${pluralize(table.rows?.length || 0, "record")}`
|
|
2714
|
+
: pluralize(table.rows?.length || 0, "record")}
|
|
2715
|
+
</span>
|
|
2716
|
+
<span className="dm-toolbar-divider" aria-hidden="true" />
|
|
2693
2717
|
<span className="dm-filter-anchor">
|
|
2694
|
-
<button type="button" className=
|
|
2718
|
+
<button type="button" className={`dm-btn-ghost${settings.filter?.clauses?.length ? " is-active" : ""}`} onClick={() => setFilterTarget((current) => current === "toolbar" ? "" : "toolbar")}>
|
|
2695
2719
|
<Filter size={13} />Filter
|
|
2720
|
+
{settings.filter?.clauses?.length > 0 && <span className="dm-filter-chip-count">{settings.filter.clauses.length}</span>}
|
|
2696
2721
|
</button>
|
|
2697
2722
|
{filterTarget === "toolbar" && (
|
|
2698
2723
|
<div className="dm-filter-popover dm-filter-popover-toolbar">
|
|
@@ -2843,6 +2868,35 @@ function DataModelTableSurface({
|
|
|
2843
2868
|
</tr>
|
|
2844
2869
|
</thead>
|
|
2845
2870
|
<tbody>
|
|
2871
|
+
{rowEntries.length === 0 && (
|
|
2872
|
+
<tr className="dm-db-empty-row">
|
|
2873
|
+
<td colSpan={visibleColumns.length + 1 + (table.mutable ? 1 : 0)}>
|
|
2874
|
+
<div className="dm-db-empty-state">
|
|
2875
|
+
{(search.trim() || settings.filter?.clauses?.length) ? (
|
|
2876
|
+
<>
|
|
2877
|
+
<strong>No records match</strong>
|
|
2878
|
+
<span>Try a different search or clear your filters.</span>
|
|
2879
|
+
<div className="dm-db-empty-actions">
|
|
2880
|
+
{search.trim() && <button type="button" className="dm-btn-outline" onClick={() => setSearch("")}>Clear search</button>}
|
|
2881
|
+
{settings.filter?.clauses?.length > 0 && <button type="button" className="dm-btn-outline" onClick={resetView}>Reset view</button>}
|
|
2882
|
+
</div>
|
|
2883
|
+
</>
|
|
2884
|
+
) : (
|
|
2885
|
+
<>
|
|
2886
|
+
<strong>No records yet</strong>
|
|
2887
|
+
<span>{table.mutable ? "Add your first row or import a CSV to start using this object." : "Records resolve at runtime for this live-backed object."}</span>
|
|
2888
|
+
{table.mutable && (
|
|
2889
|
+
<div className="dm-db-empty-actions">
|
|
2890
|
+
<button type="button" className="dm-btn-primary-sm" disabled={saving} onClick={() => onSave((config) => addTableRow(config, table))}><Plus size={13} />Add record</button>
|
|
2891
|
+
<button type="button" className="dm-btn-outline" onClick={() => setCsvOpen(true)}><Upload size={13} />Import CSV</button>
|
|
2892
|
+
</div>
|
|
2893
|
+
)}
|
|
2894
|
+
</>
|
|
2895
|
+
)}
|
|
2896
|
+
</div>
|
|
2897
|
+
</td>
|
|
2898
|
+
</tr>
|
|
2899
|
+
)}
|
|
2846
2900
|
{pageEntries.map(({ row, originalIndex }, rowIndex) => {
|
|
2847
2901
|
const visibleIndex = pageStart + rowIndex;
|
|
2848
2902
|
const displayIndex = visibleIndex + 1;
|
|
@@ -103,6 +103,21 @@ function nodeRecordName(node) {
|
|
|
103
103
|
return "";
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// Per-node run-status pill (Attio-style), docked outside the node top-right.
|
|
107
|
+
// Driven only by the real general-orchestration run signal passed in
|
|
108
|
+
// `nodeStatuses` — the streamed orchestration.node.* deltas and the persisted
|
|
109
|
+
// nodeTrace (see lib/orchestration-node-status.js). Never fabricated here.
|
|
110
|
+
const NODE_STATUS_CHIP = {
|
|
111
|
+
completed: { cls: "is-ok", label: "Completed" },
|
|
112
|
+
ok: { cls: "is-ok", label: "Completed" },
|
|
113
|
+
running: { cls: "is-running", label: "Running" },
|
|
114
|
+
executing: { cls: "is-running", label: "Running" },
|
|
115
|
+
failed: { cls: "is-bad", label: "Failed" },
|
|
116
|
+
skipped: { cls: "is-waiting", label: "Skipped" },
|
|
117
|
+
pending: { cls: "is-waiting", label: "Waiting" },
|
|
118
|
+
queued: { cls: "is-waiting", label: "Waiting" },
|
|
119
|
+
};
|
|
120
|
+
|
|
106
121
|
export function OrchestrationGraphCanvas({
|
|
107
122
|
graph,
|
|
108
123
|
selectedNodeId,
|
|
@@ -112,6 +127,8 @@ export function OrchestrationGraphCanvas({
|
|
|
112
127
|
onRunTest,
|
|
113
128
|
runStatus,
|
|
114
129
|
runMessage,
|
|
130
|
+
nodeStatuses,
|
|
131
|
+
onNodeStatusClick,
|
|
115
132
|
statusLabel = "Draft",
|
|
116
133
|
}) {
|
|
117
134
|
const parsed = useMemo(() => parseOrchestrationGraph(graph) || graph, [graph]);
|
|
@@ -222,6 +239,9 @@ export function OrchestrationGraphCanvas({
|
|
|
222
239
|
const isSelected = activeId === id;
|
|
223
240
|
const prevId = index > 0 ? String(nodes[index - 1].id || "") : "";
|
|
224
241
|
const Icon = NODE_ICONS[node.type] || ArrowDownToLine;
|
|
242
|
+
const nodeStatusChip = nodeStatuses
|
|
243
|
+
? NODE_STATUS_CHIP[String(nodeStatuses[id] || "").toLowerCase()] || null
|
|
244
|
+
: null;
|
|
225
245
|
|
|
226
246
|
return (
|
|
227
247
|
<div key={id || index} className="dm-orchestration-canvas__step">
|
|
@@ -285,6 +305,17 @@ export function OrchestrationGraphCanvas({
|
|
|
285
305
|
onSelectNode?.(node);
|
|
286
306
|
}}
|
|
287
307
|
>
|
|
308
|
+
{nodeStatusChip && (
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
className={`dm-status-chip ${nodeStatusChip.cls} dm-orchestration-node__status`}
|
|
312
|
+
title={`${nodeStatusChip.label} — open run trace`}
|
|
313
|
+
onClick={(event) => { event.stopPropagation(); onNodeStatusClick?.(node); }}
|
|
314
|
+
>
|
|
315
|
+
<span className="dm-status-dot" aria-hidden="true" />
|
|
316
|
+
{nodeStatusChip.label}
|
|
317
|
+
</button>
|
|
318
|
+
)}
|
|
288
319
|
<span className="dm-orchestration-node__icon" aria-hidden="true">
|
|
289
320
|
<Icon size={14} />
|
|
290
321
|
</span>
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
// Maps a free-form status string to one calm, consistent chip state.
|
|
4
|
+
// Backwards-compatible: same { value } prop, same default ("untested").
|
|
5
|
+
const OK = ["connected", "approved", "ok", "success", "succeeded", "complete", "completed", "passed", "live", "ready"];
|
|
6
|
+
const BAD = ["failed", "error", "errored", "disconnected", "rejected"];
|
|
7
|
+
const WARN = ["warning", "warn", "pending", "untrusted", "stale", "degraded"];
|
|
8
|
+
const RUNNING = ["running", "in_progress", "in-progress", "active", "executing", "started"];
|
|
9
|
+
const WAITING = ["waiting", "queued", "idle", "scheduled"];
|
|
10
|
+
|
|
11
|
+
function classifyStatus(status) {
|
|
12
|
+
if (OK.includes(status)) return "is-ok";
|
|
13
|
+
if (BAD.includes(status)) return "is-bad";
|
|
14
|
+
if (RUNNING.includes(status)) return "is-running";
|
|
15
|
+
if (WARN.includes(status)) return "is-warn";
|
|
16
|
+
if (WAITING.includes(status)) return "is-waiting";
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
export function StatusPill({ value }) {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const bad = ["failed", "error", "disconnected"].includes(status);
|
|
21
|
+
const raw = value || "untested";
|
|
22
|
+
const state = classifyStatus(String(raw).toLowerCase());
|
|
7
23
|
return (
|
|
8
|
-
<span className={`dm-
|
|
9
|
-
<span />
|
|
10
|
-
{
|
|
24
|
+
<span className={`dm-status-chip ${state}`.trim()}>
|
|
25
|
+
<span className="dm-status-dot" />
|
|
26
|
+
{raw}
|
|
11
27
|
</span>
|
|
12
28
|
);
|
|
13
29
|
}
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
export function ToggleField({ checked, disabled, label, onChange, description }) {
|
|
4
4
|
return (
|
|
5
|
-
<label className=
|
|
5
|
+
<label className={`dm-switch-row${disabled ? " is-disabled" : ""}`}>
|
|
6
6
|
<input
|
|
7
7
|
type="checkbox"
|
|
8
8
|
checked={Boolean(checked)}
|
|
9
9
|
disabled={disabled}
|
|
10
10
|
onChange={(event) => onChange(event.target.checked)}
|
|
11
11
|
/>
|
|
12
|
-
<span
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
<span className="dm-switch-track" aria-hidden="true" />
|
|
13
|
+
<span className="dm-switch-label">
|
|
14
|
+
<span>{label}</span>
|
|
15
|
+
{description && <span className="dm-switch-desc">{description}</span>}
|
|
15
16
|
</span>
|
|
16
17
|
</label>
|
|
17
18
|
);
|