{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "environment-variables",
  "type": "registry:component",
  "title": "Environment variables",
  "description": "Masks environment values by default, with deliberate reveal, value copy and quoted shell exports.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/button.json",
    "https://vlak.dev/r/snippet.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/environment-variables.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { Button } from \"./button\";\nimport { SnippetCopy } from \"./snippet\";\n\nexport interface EnvironmentVariable { name: string; value: string; required?: boolean; description?: string }\nexport interface EnvironmentVariablesProps extends Omit<React.HTMLAttributes<HTMLElement>, \"onCopy\" | \"title\"> {\n  variables: EnvironmentVariable[];\n  title?: React.ReactNode;\n  showValues?: boolean;\n  defaultShowValues?: boolean;\n  onShowValuesChange?: (show: boolean) => void;\n  onCopy?: (value: string) => void | Promise<void>;\n}\n\n/** POSIX shell-safe assignments. Invalid names are rejected rather than emitted as executable shell text. */\nexport function formatEnvironmentExports(variables: EnvironmentVariable[]): string {\n  return variables.map(({ name, value }) => {\n    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(\"Invalid environment variable name\");\n    return `export ${name}='${value.replaceAll(\"'\", \"'\\\\''\")}'`;\n  }).join(\"\\n\");\n}\nconst styles = stylex.create({\n  root: { minWidth: 0, width: \"100%\", borderWidth: vlak.hairline, borderStyle: \"solid\", borderColor: { default: vlak.divider, [mq.forcedColors]: \"CanvasText\" }, borderRadius: vlak.radiusSm, color: vlak.ink, backgroundColor: vlak.paper },\n  header: { display: \"flex\", flexWrap: \"wrap\", alignItems: \"center\", gap: \"0.5rem\", padding: \"0.5rem 1rem\", borderBottomWidth: vlak.hairline, borderBottomStyle: \"solid\", borderBottomColor: vlak.divider },\n  title: { flex: \"1 1 auto\", fontSize: \"0.875rem\", lineHeight: 1.45, fontWeight: 600 },\n  list: { margin: 0, padding: \"0 1rem\" },\n  row: { display: \"grid\", gridTemplateColumns: { default: \"minmax(0, 1fr) minmax(0, 1fr) auto\", [mq.phone]: \"minmax(0, 1fr) auto\" }, alignItems: \"center\", gap: \"0.5rem\", paddingBlock: \"0.5rem\", borderBottomWidth: vlak.hairline, borderBottomStyle: \"solid\", borderBottomColor: vlak.divider },\n  name: { margin: 0, minWidth: 0, overflowWrap: \"anywhere\", fontSize: \"0.875rem\", lineHeight: 1.45 },\n  value: { margin: 0, minWidth: 0, overflowWrap: \"anywhere\", fontFamily: \"ui-monospace, SFMono-Regular, Menlo, monospace\", fontSize: \"0.875rem\", lineHeight: 1.45, gridColumn: { default: \"auto\", [mq.phone]: \"1 / 2\" } },\n  controls: { display: \"flex\", flexWrap: \"wrap\", justifyContent: \"flex-end\", gap: \"0.25rem\", margin: 0, gridColumn: { default: \"auto\", [mq.phone]: \"2 / 3\" }, gridRow: { default: \"auto\", [mq.phone]: \"1 / 3\" } },\n  note: { display: \"block\", margin: 0, color: vlak.gray, fontSize: \"0.75rem\", lineHeight: 1.45 },\n  footer: { display: \"flex\", flexWrap: \"wrap\", alignItems: \"center\", gap: \"0.5rem\", padding: \"0.5rem 1rem\" },\n});\n\n/** Masked values with deliberate reveal and exact-value/export copy actions. */\nexport const EnvironmentVariables = React.forwardRef<HTMLElement, EnvironmentVariablesProps>(function EnvironmentVariables({ variables, title = \"Environment variables\", showValues, defaultShowValues = false, onShowValuesChange, onCopy, className, style, \"aria-label\": label, \"aria-labelledby\": labelledBy, ...props }, ref) {\n  const titleId = React.useId();\n  const [innerShow, setInnerShow] = React.useState(defaultShowValues);\n  const [revealed, setRevealed] = React.useState<Record<string, string>>({});\n  const visible = showValues ?? innerShow;\n  const setVisible = (show: boolean) => { if (showValues === undefined) setInnerShow(show); setRevealed({}); onShowValuesChange?.(show); };\n  let exports = \"\";\n  let validNames = true;\n  try { exports = formatEnvironmentExports(variables); } catch { validNames = false; }\n  const root = rs([\"rs-environment-variables\", className], styles.root);\n  const header = rs([\"rs-environment-variables-header\"], styles.header);\n  const titleStyle = rs([\"rs-environment-variables-title\"], styles.title);\n  const list = rs([\"rs-environment-variables-list\"], styles.list);\n  const row = rs([\"rs-environment-variables-row\"], styles.row);\n  const name = rs([\"rs-environment-variables-name\"], styles.name);\n  const valueStyle = rs([\"rs-environment-variables-value\"], styles.value);\n  const controls = rs([\"rs-environment-variables-controls\"], styles.controls);\n  const note = rs([\"rs-environment-variables-note\"], styles.note);\n  const footer = rs([\"rs-environment-variables-footer\"], styles.footer);\n  return <section {...props} ref={ref} aria-label={label} aria-labelledby={labelledBy ?? (label == null ? titleId : undefined)} className={root.className} style={{ ...root.style, ...style }}>\n    <header {...header}><span {...titleStyle} id={titleId}>{title}</span><Button type=\"button\" variant=\"subtle\" aria-pressed={visible} onClick={() => setVisible(!visible)}>{visible ? \"Hide all values\" : \"Show all values\"}</Button></header>\n    <dl {...list}>{variables.map(variable => {\n      const shown = visible || revealed[variable.name] === variable.value;\n      return <div key={variable.name} {...row}>\n        <dt {...name}><code>{variable.name}</code>{variable.required && <span {...note}>Required</span>}{variable.description && <span {...note}>{variable.description}</span>}</dt>\n        <dd {...valueStyle}>{shown ? variable.value || \"(empty)\" : <span role=\"img\" aria-label=\"Value hidden\">••••••••</span>}</dd>\n        <dd {...controls}>{!visible && <Button type=\"button\" variant=\"subtle\" aria-label={`${shown ? \"Hide\" : \"Show\"} ${variable.name}`} aria-pressed={shown} onClick={() => setRevealed(current => { const next = { ...current }; if (shown) delete next[variable.name]; else next[variable.name] = variable.value; return next; })}>{shown ? \"Hide\" : \"Show\"}</Button>}<SnippetCopy value={variable.value} label={`Copy ${variable.name}`} onCopy={onCopy} /></dd>\n      </div>;\n    })}</dl>\n    <footer {...footer}>{variables.length === 0 ? <span {...note}>No environment variables.</span> : validNames ? <><span {...note}>Shell exports</span><SnippetCopy value={exports} label=\"Copy environment exports\" onCopy={onCopy} /></> : <span {...note}>Export unavailable: variable names must contain letters, digits or underscores and cannot start with a digit.</span>}</footer>\n  </section>;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/environment-variables.tsx"
    },
    {
      "path": "vlak/styles/environment-variables.css",
      "content": "/* ── environment-variables: generated from packages/react/src/components/environment-variables.tsx ── */\n.rs-environment-variables{min-width:0;width:100%;border-width:1px;border-style:solid;border-color:var(--divider);border-radius:var(--radius-sm);color:var(--text);background-color:var(--bg)}\n@media (forced-colors: active){.rs-environment-variables{border-color:CanvasText}}\n.rs-environment-variables-header{display:flex;flex-wrap:wrap;align-items:center;gap:0.5rem;padding:0.5rem 1rem;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:var(--divider)}\n.rs-environment-variables-title{flex:1 1 auto;font-size:0.875rem;line-height:1.45;font-weight:600}\n.rs-environment-variables-list{margin:0;padding:0 1rem}\n.rs-environment-variables-row{display:grid;grid-template-columns:minmax(0, 1fr) minmax(0, 1fr) auto;align-items:center;gap:0.5rem;padding-block:0.5rem;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:var(--divider)}\n@media (max-width: 640px){.rs-environment-variables-row{grid-template-columns:minmax(0, 1fr) auto}}\n.rs-environment-variables-name{margin:0;min-width:0;overflow-wrap:anywhere;font-size:0.875rem;line-height:1.45}\n.rs-environment-variables-value{margin:0;min-width:0;overflow-wrap:anywhere;font-family:ui-monospace, SFMono-Regular, Menlo, monospace;font-size:0.875rem;line-height:1.45;grid-column:auto}\n@media (max-width: 640px){.rs-environment-variables-value{grid-column:1 / 2}}\n.rs-environment-variables-controls{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:0.25rem;margin:0;grid-column:auto;grid-row:auto}\n@media (max-width: 640px){.rs-environment-variables-controls{grid-column:2 / 3;grid-row:1 / 3}}\n.rs-environment-variables-note{display:block;margin:0;color:var(--text-secondary);font-size:0.75rem;line-height:1.45}\n.rs-environment-variables-footer{display:flex;flex-wrap:wrap;align-items:center;gap:0.5rem;padding:0.5rem 1rem}\n",
      "type": "registry:file",
      "target": "styles/vlak/environment-variables.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "ai",
      "classes": [
        "rs-environment-variables",
        "rs-environment-variables-header",
        "rs-environment-variables-title",
        "rs-environment-variables-list",
        "rs-environment-variables-row",
        "rs-environment-variables-name",
        "rs-environment-variables-value",
        "rs-environment-variables-controls",
        "rs-environment-variables-note",
        "rs-environment-variables-footer"
      ],
      "snippet": "<section class=\"rs-environment-variables\" aria-labelledby=\"environment-title\"><header class=\"rs-environment-variables-header\"><span class=\"rs-environment-variables-title\" id=\"environment-title\">Environment variables</span></header><dl class=\"rs-environment-variables-list\"><div class=\"rs-environment-variables-row\"><dt class=\"rs-environment-variables-name\"><code>MODEL_NAME</code></dt><dd class=\"rs-environment-variables-value\"><span role=\"img\" aria-label=\"Value hidden\">••••••••</span></dd></div></dl></section>",
      "cssOnly": false,
      "registryDependencies": [
        "button",
        "snippet",
        "vlak-lib"
      ],
      "aliases": [
        "AI Elements Environment Variables",
        "Environment settings",
        "Secret values"
      ],
      "example": "import { EnvironmentVariables } from \"@noorddev/vlak-react\";\n\n<EnvironmentVariables variables={[{ name: \"MODEL_NAME\", value: \"workspace-model\", required: true }, { name: \"EXAMPLE_TOKEN\", value: \"example-only\" }]} />",
      "usage": {
        "use": [
          "Review supplied settings with masked initial values and explicit reveal controls.",
          "Use showValues and onShowValuesChange for controlled visibility; per-row reveals reset when the value changes.",
          "Copy a value or all portable shell exports. Export generation rejects invalid assignment names and quotes shell metacharacters."
        ],
        "avoid": [
          "Treating visual masking as secret storage; values still exist in application memory.",
          "Using duplicate variable names or copying secret values without a user action."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Tab reaches show-all, per-row reveal and copy controls."
        },
        {
          "keys": "Enter, Space",
          "does": "Enter or Space activates a focused control; the show-all button exposes its pressed state."
        }
      ],
      "a11y": [
        "The section and every reveal/copy control have descriptive names.",
        "Hidden values are absent from rendered text and labelled as hidden; values are never placed in title or data attributes.",
        "Clipboard success follows the resolved write, failures remain actionable, and pending writes cannot report success for changed data."
      ]
    }
  }
}
