diff --git a/package-lock.json b/package-lock.json index 00b6bca..935d017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "vitest": "^3.2.4" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19" } }, "node_modules/@adobe/css-tools": { diff --git a/package.json b/package.json index ee280c8..7e0e818 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@vitest/coverage-v8": "^3.2.4" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19" }, "dependencies": { "@heroicons/react": "^2.2.0", diff --git a/src/CodeBlock/CodeBlock.stories.tsx b/src/CodeBlock/CodeBlock.stories.tsx index d7ffe5b..e20d476 100644 --- a/src/CodeBlock/CodeBlock.stories.tsx +++ b/src/CodeBlock/CodeBlock.stories.tsx @@ -11,9 +11,16 @@ const meta = { control: "select", options: [...highlightLanguages], }, + kind: { + control: "select", + options: ["snippet", "file"], + }, code: { control: "text" }, allowCopy: { control: "boolean" }, }, + args: { + kind: "snippet", + }, } satisfies Meta; export default meta; @@ -55,3 +62,54 @@ jobs: allowCopy: true, }, }; +export const GithubCI: Story = { + args: { + language: "yaml", + code: `on: + pull_request: + workflow_dispatch: + push: + branches: + - main + +jobs: + nix-ci: + runs-on: ubuntu-latest + # Include this block to log in to FlakeHub and access private flakes + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/determinate-nix-action@v3 + - uses: DeterminateSystems/flakehub-cache-action@main + - uses: DeterminateSystems/nix-flake-checker-action@main + - run: nix flake check`, + title: ".github/workflows/nix-ci.yaml", + kind: "file", + allowCopy: true, + allowDownload: true, + }, +}; + +export const AWS: Story = { + args: { + language: "terraform", + code: `data "aws_ami" "detsys_nixos" { + owners = ["535002876703"] + most_recent = true + + filter { + name = "name" + values = ["determinate/nixos/epoch-1/*"] + } + + filter { + name = "architecture" + values = ["x86_64"] # or "ARM64" for Graviton + } +}`, + title: "aws_ami.tf", + kind: "file", + }, +}; diff --git a/src/CodeBlock/index.scss b/src/CodeBlock/index.scss index 3a49997..d1d463b 100644 --- a/src/CodeBlock/index.scss +++ b/src/CodeBlock/index.scss @@ -2,7 +2,38 @@ @use "../sass/mixins"; @use "../sass/tokens"; +@use "../sass/functions"; + +.code-block { + @include mixins.border(base); + @include mixins.margin-y(xl); + + .highlight { + @include mixins.border-bottom(base); + @include mixins.pad(base); + } +} .code-block__heading { - @include mixins.pad-bottom(lg); + @include mixins.border-top(base); + + padding: map.get(tokens.$spacing, base) map.get(tokens.$spacing, lg); + text-align: center; + align-items: center; + justify-content: space-between; + display: flex; + + @include mixins.light-mode { + color: map.get(tokens.$brand, black); + } + + background: functions.lighten(map.get(tokens.$brand, black), 5%); + @include mixins.light-mode { + background: functions.darken(map.get(tokens.$brand, white), 5%); + } + background-repeat: no-repeat; + + .code-block__title--file { + font-family: map.get(tokens.$fonts, mono); + } } diff --git a/src/CodeBlock/index.tsx b/src/CodeBlock/index.tsx index 5cd7635..1ed4ac5 100644 --- a/src/CodeBlock/index.tsx +++ b/src/CodeBlock/index.tsx @@ -5,12 +5,17 @@ import Highlight from "../Highlight"; import CopyButton from "../CopyButton"; import "./index.scss"; +import DownloadButton from "../DownloadButton"; +import clsx from "clsx"; export interface CodeBlockProps { language: HighlightLanguage; code: string; title: string; + downloadAs?: string; allowCopy?: boolean; + allowDownload?: boolean; + kind: "snippet" | "file"; } /** @@ -20,14 +25,40 @@ const CodeBlock: FC = ({ language, code, title, + downloadAs = title, + allowDownload = true, allowCopy = true, -}) => ( -
-
- {title} {allowCopy && } -
- -
-); + kind = "snippet", +}) => { + const trimmedCode = code.trim(); + const isFile = kind === "file"; + + return ( +
+
+ + {title} + + {(allowCopy || allowDownload) && ( + + {allowCopy && } + {allowDownload && isFile && ( + + )} + + )} +
+ +
+ ); +}; export default CodeBlock; diff --git a/src/CodeFile/CodeFile.stories.tsx b/src/CodeFile/CodeFile.stories.tsx deleted file mode 100644 index a788384..0000000 --- a/src/CodeFile/CodeFile.stories.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import CodeFile from "./"; -import { highlightLanguages } from "../hooks/useHighlight"; - -const meta = { - title: "Molecules/CodeFile", - component: CodeFile, - argTypes: { - language: { - control: "select", - options: [...highlightLanguages], - }, - code: { control: "text" }, - filename: { control: "text" }, - allowCopy: { control: "boolean" }, - allowDownload: { control: "boolean" }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const GithubCI: Story = { - args: { - language: "yaml", - code: `on: - pull_request: - workflow_dispatch: - push: - branches: - - main - -jobs: - nix-ci: - runs-on: ubuntu-latest - # Include this block to log in to FlakeHub and access private flakes - permissions: - id-token: write - contents: read - steps: - - uses: actions/checkout@v4 - - uses: DeterminateSystems/determinate-nix-action@v3 - - uses: DeterminateSystems/flakehub-cache-action@main - - uses: DeterminateSystems/nix-flake-checker-action@main - - run: nix flake check`, - filename: ".github/workflows/nix-ci.yaml", - download: "github-nix-ci.yaml", - allowCopy: true, - allowDownload: true, - }, -}; - -export const AWS: Story = { - args: { - language: "terraform", - code: `data "aws_ami" "detsys_nixos" { - owners = ["535002876703"] - most_recent = true - - filter { - name = "name" - values = ["determinate/nixos/epoch-1/*"] - } - - filter { - name = "architecture" - values = ["x86_64"] # or "ARM64" for Graviton - } -}`, - filename: "aws_ami.tf", - allowCopy: true, - allowDownload: true, - }, -}; diff --git a/src/CodeFile/index.scss b/src/CodeFile/index.scss deleted file mode 100644 index 26a9387..0000000 --- a/src/CodeFile/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -@use "sass:map"; - -@use "../sass/tokens"; -@use "../sass/mixins"; - -.code-file__heading code { - font-family: map.get(tokens.$fonts, mono); - color: map.get(tokens.$brand, white); - @include mixins.light-mode { - color: map.get(tokens.$brand, black); - } -} diff --git a/src/CodeFile/index.tsx b/src/CodeFile/index.tsx deleted file mode 100644 index 6caae06..0000000 --- a/src/CodeFile/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { type FC } from "react"; - -import type { HighlightLanguage } from "../hooks/useHighlight"; -import CopyButton from "../CopyButton"; -import DownloadButton from "../DownloadButton"; -import Highlight from "../Highlight"; - -import "./index.scss"; - -export interface CodeFileProps { - language: HighlightLanguage; - code: string; - - /** - * The filename to use for displaying. - */ - filename: string; - - /** - * If an alternative download filename is required (for example, a user might - * download a dot-prefixed filename that is hidden by default on macOS and - * Linux). - * - * Defaults to the value of `filename` - */ - download?: string; - - /** - * Whether or not to allow copying. Defaults to true. - */ - allowCopy?: boolean; - - /** - * Whether or not to allow downloading. Defaults to true. - */ - allowDownload?: boolean; -} - -const CodeFile: FC = ({ - language, - code, - filename, - download = filename, - allowCopy, - allowDownload, -}) => ( -
-
- {filename} {allowCopy && }{" "} - {allowDownload && } -
- -
-); - -export default CodeFile; diff --git a/src/ColorProvider/index.tsx b/src/ColorProvider/index.tsx index a351c8f..cc59c1d 100644 --- a/src/ColorProvider/index.tsx +++ b/src/ColorProvider/index.tsx @@ -94,9 +94,15 @@ const ColorProvider: React.FC> = ({ useLocalStorage = true, simulatedSystemColorScheme, preferredColorScheme, - root = document.body, + root, children, }) => { + if (!root) { + if (typeof document !== "undefined") { + root = document.body; + } + } + const actualSystemColorScheme = useSystemColorScheme(); const systemColorScheme = simulatedSystemColorScheme ?? actualSystemColorScheme; @@ -109,7 +115,9 @@ const ColorProvider: React.FC> = ({ ); // Apply the theme super early so we don't get a FOUC - applyTheme(root, scheme); + if (root) { + applyTheme(root, scheme); + } // Since we're pretty high up in the component tree, we want to be extremely // careful about re-rendering. Memoization ensures that the object only @@ -127,7 +135,9 @@ const ColorProvider: React.FC> = ({ // Switch body classes depending on the chosen scheme useEffect(() => { - applyTheme(root, scheme); + if (root) { + applyTheme(root, scheme); + } }, [scheme]); return ( @@ -137,6 +147,11 @@ const ColorProvider: React.FC> = ({ function applyTheme(root: Element, scheme: ColorScheme) { const classes = root.classList; + if (classes === undefined) { + console.log("classList on the root is null... probably SSR?"); + return; + } + const [next, previous] = scheme === "light" ? ["light", "dark"] : ["dark", "light"]; diff --git a/src/Header/index.tsx b/src/Header/index.tsx index 6e0c850..d608d03 100644 --- a/src/Header/index.tsx +++ b/src/Header/index.tsx @@ -5,20 +5,24 @@ import React from "react"; export interface HeaderProps { logo?: React.ReactNode; - elements?: React.ReactNode[]; + elements?: React.ReactElement[]; } const Header: FC = ({ logo, elements = [] }) => { return ( -
+
{!!logo &&
{logo}
} {elements.length > 0 && ( -
{elements}
+
+ {elements.map((node, idx) => ( +
{node}
+ ))} +
)}
-
+ ); }; diff --git a/src/Highlight/index.scss b/src/Highlight/index.scss index f639e31..8b0e470 100644 --- a/src/Highlight/index.scss +++ b/src/Highlight/index.scss @@ -4,16 +4,14 @@ @use "../sass/mixins"; // Default values taken from Shiki's 'classic' output type -$shiki-dark-bg: #24292e; -$shiki-light-bg: #fff; .highlight { text-wrap-mode: wrap; line-break: anywhere; - background-color: var(--shiki-dark-bg, map.get(tokens.$code, bg-dark)); + background-color: map.get(tokens.$code, bg-dark); @include mixins.light-mode { - background-color: var(--shiki-light-bg, map.get(tokens.$code, bg-light)); + background-color: map.get(tokens.$code, bg-light); } &.highlight--inline { diff --git a/src/InstallCTA/FindAwsAMIs.tsx b/src/InstallCTA/FindAwsAMIs.tsx new file mode 100644 index 0000000..557ac3a --- /dev/null +++ b/src/InstallCTA/FindAwsAMIs.tsx @@ -0,0 +1,36 @@ +import type { FC } from "react"; +import CodeBlock from "../CodeBlock"; +import Link from "../Link"; +import type { TabProps } from "."; + +const code = `aws ec2 describe-images \\ + --owners 535002876703 \\ + --filters "Name=name,Values=determinate/nixos/epoch-1/*" \\ + "Name=architecture,Values=x86_64,arm64" \\ + --query "Images | sort_by(@, &CreationDate) | [-1]"`; + +const FindAwsAMIs: FC = (_: TabProps) => { + return ( + <> +

+ Use NixOS with Determinate on AWS with our pre-published AMIs in every + region. +

+ +

+ See{" "} + + DeterminateSystems/nixos-amis on GitHub + + . +

+ + ); +}; + +export default FindAwsAMIs; diff --git a/src/InstallCTA/InstallCTA.stories.tsx b/src/InstallCTA/InstallCTA.stories.tsx index 92e0c82..1910150 100644 --- a/src/InstallCTA/InstallCTA.stories.tsx +++ b/src/InstallCTA/InstallCTA.stories.tsx @@ -6,7 +6,21 @@ import { InstallTarget } from "./types"; const meta = { title: "Composite/InstallCTA", component: InstallCTA, - argTypes: {}, + argTypes: { + initialTab: { + type: "string", + options: [ + InstallTarget.AWS, + InstallTarget.GitHub, + InstallTarget.Linux, + InstallTarget.MacOS, + InstallTarget.NixOS, + ], + }, + version: { + type: "string", + }, + }, } satisfies Meta; export default meta; @@ -16,21 +30,73 @@ export const Default: Story = { args: {}, }; -export const MacOS: Story = { - name: "macOS", +export const MacOSLatest: Story = { + name: "macOS Tagged", args: { initialTab: InstallTarget.MacOS, }, }; -export const WSL: Story = { +export const LinuxLatest: Story = { + args: { + initialTab: InstallTarget.Linux, + }, +}; + +export const AWSLatest: Story = { + args: { + initialTab: InstallTarget.AWS, + }, +}; + +export const GitHubLatest: Story = { + name: "GitHub Latest", + args: { + initialTab: InstallTarget.GitHub, + }, +}; + +export const NixOSLatest: Story = { + name: "NixOS Latest", args: { - initialTab: InstallTarget.WSL, + initialTab: InstallTarget.NixOS, }, }; -export const Linux: Story = { +export const MacOSTagged: Story = { + name: "macOS Tagged", + args: { + initialTab: InstallTarget.MacOS, + version: "3.8.6", + }, +}; + +export const LinuxTagged: Story = { args: { initialTab: InstallTarget.Linux, + version: "3.8.6", + }, +}; + +export const AWSTagged: Story = { + args: { + initialTab: InstallTarget.AWS, + version: "3.8.6", + }, +}; + +export const GitHubTagged: Story = { + name: "GitHub Tagged", + args: { + initialTab: InstallTarget.GitHub, + version: "3.8.6", + }, +}; + +export const NixOSTagged: Story = { + name: "NixOS Tagged", + args: { + initialTab: InstallTarget.NixOS, + version: "3.8.6", }, }; diff --git a/src/InstallCTA/InstallForNixOS.tsx b/src/InstallCTA/InstallForNixOS.tsx new file mode 100644 index 0000000..848f898 --- /dev/null +++ b/src/InstallCTA/InstallForNixOS.tsx @@ -0,0 +1,51 @@ +import type { FC } from "react"; +import CodeBlock from "../CodeBlock"; +import Link from "../Link"; +import type { TabProps } from "."; + +const code = ` +nixosConfigurations.workstation = nixpkgs.lib.nixosSystem { + modules = [ + # Load the Determinate module + determinate.nixosModules.default + ./configuration.nix + ]; +}; +`; + +const InstallForNixOS: FC = ({ version }: TabProps) => { + return ( + <> +

Add Determinate as an input to your flake:

+ +

Then, include the Determinate NixOS module:

+ + +

+ For more details, see{" "} + + Install on NixOS + + . +

+ + ); +}; + +export default InstallForNixOS; diff --git a/src/InstallCTA/InstallFromCurl.tsx b/src/InstallCTA/InstallFromCurl.tsx index 7a00817..a163d3f 100644 --- a/src/InstallCTA/InstallFromCurl.tsx +++ b/src/InstallCTA/InstallFromCurl.tsx @@ -1,16 +1,26 @@ import { type FC } from "react"; import CodeBlock from "../CodeBlock"; +import type { TabProps } from "."; -const installScript = - "curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate"; +const installScript = (version: string | undefined) => { + if (typeof version === "undefined") { + return "curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate"; + } else { + return `curl -fsSL https://install.determinate.systems/nix/tag/v${version} | sh -s -- install --determinate`; + } +}; -const InstallFromCurl: FC = () => ( +const InstallFromCurl: FC = ({ version }: TabProps) => ( <> -

Use the Determinate Nix Installer CLI

+

+ Use the Determinate Nix Installer CLI for Linux, including Windows + Subsystem for Linux (WSL). +

diff --git a/src/InstallCTA/NavTab.tsx b/src/InstallCTA/NavTab.tsx deleted file mode 100644 index af46bd0..0000000 --- a/src/InstallCTA/NavTab.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { FC } from "react"; - -import type { InstallTarget } from "./types"; - -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/16/solid"; - -export interface NavTabProps { - name: InstallTarget; - icon: FC; - href: string; - external?: boolean; -} - -const NavTab: FC = ({ - name, - icon: Icon, - href, - external = false, -}) => ( -
  • - - {name} - - -
  • -); - -export default NavTab; diff --git a/src/InstallCTA/TabSelector.tsx b/src/InstallCTA/TabSelector.tsx index 4998f43..4380a96 100644 --- a/src/InstallCTA/TabSelector.tsx +++ b/src/InstallCTA/TabSelector.tsx @@ -1,10 +1,11 @@ import { useCallback, type FC, type MouseEvent } from "react"; import type { InstallTarget } from "./types"; +import type { IconBaseProps } from "react-icons"; export interface TabSelectorProps { name: InstallTarget; - icon: FC; + icon: FC; active: boolean; onClick: () => void; } @@ -34,7 +35,7 @@ const TabSelector: FC = ({ } onClick={handleClick} > - {name} + {name} ); diff --git a/src/InstallCTA/UseWithGitHubActions.tsx b/src/InstallCTA/UseWithGitHubActions.tsx new file mode 100644 index 0000000..2319249 --- /dev/null +++ b/src/InstallCTA/UseWithGitHubActions.tsx @@ -0,0 +1,34 @@ +import type { FC } from "react"; +import Link from "../Link"; +import CodeBlock from "../CodeBlock"; +import type { TabProps } from "."; + +const UseWithGitHubActions: FC = ({ version }: TabProps) => { + const code = ` +on: + push: +jobs: + tests: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: DeterminateSystems/determinate-nix-action@v${version || "3"} + - run: nix build .# + +`; + return ( + <> +

    Use Determinate Nix in GitHub Actions.

    + +

    + See our other{" "} + + GitHub Actions + + . +

    + + ); +}; + +export default UseWithGitHubActions; diff --git a/src/InstallCTA/index.scss b/src/InstallCTA/index.scss index 6aff8bd..ae2a4e6 100644 --- a/src/InstallCTA/index.scss +++ b/src/InstallCTA/index.scss @@ -2,8 +2,29 @@ @use "../sass/tokens"; @use "../sass/mixins"; +@use "../sass/functions"; .install-cta { + @include mixins.border(base); + @include mixins.shadow; + + width: 100%; + + .install-cta__header { + @include mixins.pad(lg); + + font-weight: 600; + font-size: map.get(tokens.$font-size, lg); + color: map.get(tokens.$brand, light); + @include mixins.light-mode { + color: map.get(tokens.$brand, gray); + } + } + + p { + font-weight: 300; + } + strong, p { color: map.get(tokens.$brand, white); @@ -13,32 +34,67 @@ } } +.install-cta__body { + @include mixins.pad(lg); + margin: map.get(tokens.$spacing, xl) 0; + + p { + font-size: map.get(tokens.$font-size, base); + } +} + .install-cta__links { + @include mixins.pad(lg); display: flex; flex-direction: row; + flex-wrap: wrap; + text-align: center; + align-items: center; + gap: map.get(tokens.$spacing, base); list-style-type: none; - margin: 0; - padding: 0; } -.install-cta__tab { - padding: map.get(tokens.$spacing, xs) map.get(tokens.$spacing, xs); +.install-cta__link { + display: flex; + text-align: center; + align-items: center; + text-wrap-mode: nowrap; - display: inline-flex; -} + @include mixins.transition(border-bottom-color, shortish); + @include mixins.transition(color, shortish); -.install-cta__link { - @include mixins.transition(border-top-color, shortish); - border-top: 2px solid rgba($color: blue, $alpha: 0); + color: functions.darken(map.get(tokens.$brand, white), 15%); + @include mixins.light-mode { + color: functions.lighten(map.get(tokens.$brand, black), 30%); + } + border-bottom: 2px solid + rgba($color: map.get(tokens.$colors, secondary), $alpha: 0); + padding: map.get(tokens.$spacing, lg) map.get(tokens.$spacing, xs); + + svg { + padding-right: map.get(tokens.$padding, base); + height: 20px; + width: auto; + } - &.install-cta__link--active { - border-top-color: blue; + &:hover { + color: map.get(tokens.$brand, white); + border-bottom-color: map.get(tokens.$colors, secondary); + @include mixins.light-mode { + color: map.get(tokens.$brand, very-black); + } } - &:hover, + &.install-cta__link--active, &:focus { - border-top-color: red; + color: map.get(tokens.$brand, sky-blue); + border-bottom-color: map.get(tokens.$brand, sky-blue); + + @include mixins.light-mode { + color: map.get(tokens.$brand, ocean-blue); + border-bottom-color: map.get(tokens.$brand, ocean-blue); + } } } diff --git a/src/InstallCTA/index.tsx b/src/InstallCTA/index.tsx index 186f3b2..1c17e17 100644 --- a/src/InstallCTA/index.tsx +++ b/src/InstallCTA/index.tsx @@ -1,13 +1,18 @@ import { useState, type FC } from "react"; +import { SiGithub, SiLinux, SiApple, SiNixos, SiAmazon } from "react-icons/si"; import TabSelector from "./TabSelector"; import { InstallTarget, detectInstallTarget } from "./types"; import InstallFromCurl from "./InstallFromCurl"; +import FindAwsAMIs from "./FindAwsAMIs"; +import UseWithGitHubActions from "./UseWithGitHubActions"; +import InstallForNixOS from "./InstallForNixOS"; + import MacInstaller from "../MacInstaller"; import "./index.scss"; -import NavTab from "./NavTab"; +import type { IconBaseProps } from "react-icons"; export { InstallTarget as CTAType } from "./types"; @@ -17,14 +22,42 @@ export interface InstallCTAProps { * This is only applicable for the initial render. */ initialTab?: InstallTarget; + + /** + * Version of Determinate to pin to, when possible + */ + version?: string; } +export interface TabProps { + /** + * Version of Determinate to pin to, when possible + */ + version?: string; +} + +const ctaTabs: [InstallTarget, React.FC][] = [ + [InstallTarget.MacOS, SiApple], + [InstallTarget.Linux, SiLinux], + [InstallTarget.NixOS, SiNixos], + [InstallTarget.AWS, SiAmazon], + [InstallTarget.GitHub, SiGithub], +]; + +const ctaComponents: Record> = { + [InstallTarget.MacOS]: MacInstaller, + [InstallTarget.Linux]: InstallFromCurl, + [InstallTarget.AWS]: FindAwsAMIs, + [InstallTarget.GitHub]: UseWithGitHubActions, + [InstallTarget.NixOS]: InstallForNixOS, +}; + /** * A call-to-action component for downloading Determinate Nix. * * Due to the numerous options available, */ -const InstallCTA: FC = ({ initialTab }) => { +const InstallCTA: FC = ({ initialTab, version }) => { const [activeTab, setActiveTab] = useState(() => { if (initialTab) { return initialTab; @@ -33,44 +66,26 @@ const InstallCTA: FC = ({ initialTab }) => { return detectInstallTarget(); }); - const wantsCurlInstall = - activeTab === InstallTarget.WSL || activeTab === InstallTarget.Linux; + const TabBody = ctaComponents[activeTab]; return (
    -

    - Get Determinate for {activeTab} -

    -
    - {activeTab === InstallTarget.MacOS && } - {wantsCurlInstall && } -
    +
    + Get Determinate{version && ` v${version}`} +
      - null} - active={activeTab === InstallTarget.MacOS} - onClick={() => setActiveTab(InstallTarget.MacOS)} - /> - null} - active={activeTab === InstallTarget.WSL} - onClick={() => setActiveTab(InstallTarget.WSL)} - /> - null} - active={activeTab === InstallTarget.Linux} - onClick={() => setActiveTab(InstallTarget.Linux)} - /> - null} - href="https://docs.determinate.systems/guides/advanced-installation/#nixos" - external={true} - /> + {ctaTabs.map(([target, icon]) => ( + setActiveTab(target)} + /> + ))}
    +
    + +
    ); }; diff --git a/src/InstallCTA/types.ts b/src/InstallCTA/types.ts index 6099ed8..4a45fba 100644 --- a/src/InstallCTA/types.ts +++ b/src/InstallCTA/types.ts @@ -3,9 +3,10 @@ */ export enum InstallTarget { MacOS = "macOS", - WSL = "Windows Subsystem for Linux (WSL)", Linux = "Linux", NixOS = "NixOS", + AWS = "AWS", + GitHub = "GitHub", } /** @@ -17,7 +18,7 @@ export function detectInstallTarget( userAgent = navigator.userAgent, ): InstallTarget { if (userAgent.includes("Windows")) { - return InstallTarget.WSL; + return InstallTarget.Linux; } if (userAgent.includes("Linux")) { diff --git a/src/Link/index.scss b/src/Link/index.scss index b57d28e..71cd5f2 100644 --- a/src/Link/index.scss +++ b/src/Link/index.scss @@ -5,6 +5,8 @@ .link { color: map.get(tokens.$brand, orange); + text-decoration: underline; + text-underline-offset: 0.15em; @include mixins.transition(color, short); &:hover { diff --git a/src/MacInstaller/index.scss b/src/MacInstaller/index.scss index ae3693b..db14d8e 100644 --- a/src/MacInstaller/index.scss +++ b/src/MacInstaller/index.scss @@ -10,13 +10,25 @@ min-width: 15em; align-items: center; + justify-content: center; background-color: map.get(tokens.$brand, black); - border: 1px solid map.get(tokens.$brand, magenta); + @include mixins.light-mode { + background-color: map.get(tokens.$brand, light); + } + color: map.get(tokens.$brand, white); + @include mixins.light-mode { + color: map.get(tokens.$brand, black); + } + + border: 1px solid map.get(tokens.$brand, magenta); + text-decoration: none; font-family: map.get(tokens.$fonts, sans); + font-size: map.get(tokens.$font-size, base); + font-weight: 500; padding: 1rem; border-radius: 0.5rem; @@ -29,10 +41,17 @@ map.get(tokens.$brand, magenta), $blackness: 50% ); + @include mixins.light-mode { + background-color: color.scale( + map.get(tokens.$brand, magenta), + $lightness: 80% + ); + } } } .mac-installer__pkg { flex: 3.5rem 3.5rem auto; margin-right: 1rem; + width: 45px; } diff --git a/src/MacInstaller/index.tsx b/src/MacInstaller/index.tsx index 2ecc7cc..79df065 100644 --- a/src/MacInstaller/index.tsx +++ b/src/MacInstaller/index.tsx @@ -4,21 +4,30 @@ import url from "./macPackageData"; import "./index.scss"; +export interface MacInstallerProps { + version?: string; +} + /** * A call to action for downloading the Determinate Nix universal Mac installer. */ -const MacInstaller: FC = () => ( - - Package icon representing the graphical installer - Download Determinate Nix - -); +const MacInstaller: FC = ({ + version, +}: MacInstallerProps) => { + const urlPath = version ? `tag/v${version}` : `stable`; + return ( + + Package icon representing the graphical installer + Download Determinate Nix{version && ` v${version}`} + + ); +}; export default MacInstaller; diff --git a/src/Navigation/Navigation.stories.tsx b/src/Navigation/Navigation.stories.tsx new file mode 100644 index 0000000..159d656 --- /dev/null +++ b/src/Navigation/Navigation.stories.tsx @@ -0,0 +1,107 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import Navigation from "."; +import { Placeholder } from "../Placeholder"; +import Header from "../Header"; +import ColorSchemeToggle from "../ColorSchemeToggle"; +import DetSysIcon from "../DetSysIcon"; + +const meta = { + title: "Molecules/Navigation", + component: Navigation, + argTypes: { + initialState: { + options: ["open", "closed"], + }, + children: { type: "function" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + initialState: undefined, + children: [ + , + , + ], + }, + render: (props) => ( +
    + +
    + ), +}; + +export const InHeader: Story = { + args: { + initialState: undefined, + children: [ + , + , + ], + }, + render: (props) => ( +
    } + elements={[ + , + , + ]} + /> + ), +}; + +export const DefaultOpen: Story = { + args: { + initialState: "open", + children: [ + , + , + ], + }, + render: (props) => ( + <> +
    + +
    + + ), +}; + +export const InHeaderOpen: Story = { + args: { + initialState: "open", + children: [ + , + , + ], + }, + render: (props) => ( +
    } + elements={[ + , + , + ]} + /> + ), +}; diff --git a/src/Navigation/index.scss b/src/Navigation/index.scss new file mode 100644 index 0000000..115e1df --- /dev/null +++ b/src/Navigation/index.scss @@ -0,0 +1,67 @@ +@use "sass:map"; + +@use "../sass/mixins"; +@use "../sass/tokens"; +@use "../sass/functions"; + +@media screen and (width >= 1024px) { + .navigation { + height: 24px; // Matches the svg-icon height used by the expander in closed mode + padding: 4px; // Matches the svg-icon height used by the expander in closed mode + .navigation__expand { + display: none; + } + + .navigation__elements { + height: 24px; + display: flex; + flex-direction: row; + gap: 1em; + } + } +} + +@media screen and (width < 1024px) { + .navigation { + position: relative; + + .navigation__expand { + @include mixins.svg-button; + } + + .navigation__elements { + display: flex; + flex-direction: column; + gap: 1em; + + max-width: 100vw; + + position: absolute; + transform-origin: 100% 0; + z-index: 10; + right: 0; + + margin-top: 0.5em; + + @include mixins.pad(base); + @include mixins.border(base); + + background-color: map.get(tokens.$brand, gray); + @include mixins.light-mode { + background-color: map.get(tokens.$brand, light); + } + } + + .navigation__element { + width: 320px; + + @media screen and (width < 650px) { + width: 268px; + } + } + } + + .navigation--closed .navigation__elements { + display: none; + } +} diff --git a/src/Navigation/index.tsx b/src/Navigation/index.tsx new file mode 100644 index 0000000..b29b5af --- /dev/null +++ b/src/Navigation/index.tsx @@ -0,0 +1,57 @@ +import { useId, useState, type FC } from "react"; + +import "./index.scss"; +import React from "react"; +import clsx from "clsx"; +import { Bars3Icon } from "@heroicons/react/24/outline"; + +export interface NavigationProps { + initialState?: "open" | "closed"; + children: React.ReactNode[]; +} + +const Navigation: FC = ({ + children, + initialState = "closed", +}) => { + const navigationId = useId(); + + const [menuState, setMenuState] = useState(initialState === "open"); + + const toggleNavigation = () => { + setMenuState((prev) => !prev); + }; + + return ( + + ); +}; + +export default Navigation; diff --git a/src/PageLayout/PageLayout.stories.tsx b/src/PageLayout/PageLayout.stories.tsx index cd17b03..8e8c185 100644 --- a/src/PageLayout/PageLayout.stories.tsx +++ b/src/PageLayout/PageLayout.stories.tsx @@ -2,6 +2,10 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import PageLayout from "."; import { Placeholder } from "../Placeholder"; +import Header from "../Header"; +import DetSysIcon from "../DetSysIcon"; +import ColorSchemeToggle from "../ColorSchemeToggle"; +import Navigation from "../Navigation"; const meta = { title: "Template/PageLayout", @@ -33,3 +37,30 @@ export const Default: Story = { content: , }, }; + +export const FleshedOut: Story = { + args: { + header: ( +
    + +

    DetSys

    + + } + elements={[ + , + , + , + ]} + />, + ]} + /> + ), + content: , + panes: [], + }, +}; diff --git a/src/PageLayout/index.tsx b/src/PageLayout/index.tsx index 897d72d..e7c4f0c 100644 --- a/src/PageLayout/index.tsx +++ b/src/PageLayout/index.tsx @@ -23,7 +23,13 @@ const PageLayout: FC = ({ {!!header &&
    {header}
    }
    {content}
    - {panes.length > 0 &&
    {panes}
    } + {panes.length > 0 && ( +
    + {panes.map((pane, idx) => ( +
    {pane}
    + ))} +
    + )}
    {!!footer &&
    {footer}
    } diff --git a/src/hooks/useHighlight.ts b/src/hooks/useHighlight.ts index 3830e90..87cb2e0 100644 --- a/src/hooks/useHighlight.ts +++ b/src/hooks/useHighlight.ts @@ -3,6 +3,7 @@ import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript"; import langShell from "@shikijs/langs/shellscript"; import langTerraform from "@shikijs/langs/terraform"; import langYaml from "@shikijs/langs/yaml"; +import langNix from "@shikijs/langs/nix"; import themeGitHubDark from "@shikijs/themes/github-dark"; import themeGitHubLight from "@shikijs/themes/github-light"; import { useMemo } from "react"; @@ -11,10 +12,11 @@ import { useMemo } from "react"; * Languages understood by the UI system's highlighter. The `text` option prevents highlighting. */ export const highlightLanguages = [ + "nix", "shell", - "yaml", "terraform", "text", + "yaml", ] as const; export type HighlightLanguage = (typeof highlightLanguages)[number]; @@ -23,7 +25,7 @@ let shiki: ReturnType; function getShiki() { return (shiki ??= createHighlighterCoreSync({ themes: [themeGitHubLight, themeGitHubDark], - langs: [langShell, langTerraform, langYaml], + langs: [langNix, langShell, langTerraform, langYaml], engine: createJavaScriptRegexEngine(), })); } diff --git a/src/hooks/useSystemColorScheme.ts b/src/hooks/useSystemColorScheme.ts index ace382a..3a8ec52 100644 --- a/src/hooks/useSystemColorScheme.ts +++ b/src/hooks/useSystemColorScheme.ts @@ -1,8 +1,12 @@ -import React from "react"; +import { useEffect, useState } from "react"; import type { ColorScheme } from "../ColorContext"; function darkModeMatcher(): MediaQueryList | undefined { - const media = window?.matchMedia?.("(prefers-color-scheme: dark)"); + if (typeof window === "undefined") { + return undefined; + } + + const media = window.matchMedia?.("(prefers-color-scheme: dark)"); if (!media) { return undefined; } @@ -24,11 +28,11 @@ function querySystemColorScheme(): ColorScheme | undefined { } function useSystemColorScheme() { - const [systemColorScheme, setSystemColorScheme] = React.useState( + const [systemColorScheme, setSystemColorScheme] = useState( querySystemColorScheme, ); - React.useEffect(() => { + useEffect(() => { const media = darkModeMatcher(); if (!media) { diff --git a/src/index.ts b/src/index.ts index 5b7b1ec..85d7cf2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,8 +14,6 @@ export { type ButtonProps, default as Button } from "./Button"; export { type CodeBlockProps, default as CodeBlock } from "./CodeBlock"; -export { type CodeFileProps, default as CodeFile } from "./CodeFile"; - export { type ColorContextValue, type ColorScheme, @@ -76,3 +74,5 @@ export { } from "./LabeledTextInput"; export { type PageLayoutProps, default as PageLayout } from "./PageLayout"; + +export { type NavigationProps, default as Navigation } from "./Navigation"; diff --git a/src/sass/_mixins.scss b/src/sass/_mixins.scss index 589ad17..9a9ddf9 100644 --- a/src/sass/_mixins.scss +++ b/src/sass/_mixins.scss @@ -28,11 +28,83 @@ } } +@mixin border($size) { + border-width: 1px; + border-style: solid; + border-radius: map.get(tokens.$border-radius, $size); + + border-color: map.get(tokens.$border-color, dark); + @include light-mode { + border-color: map.get(tokens.$border-color, light); + } +} + +@mixin border-top($size) { + border-top-width: 1px; + border-top-style: solid; + border-left-style: solid; + border-right-style: solid; + border-left-width: 1px; + border-right-width: 1px; + border-top-left-radius: calc(map.get(tokens.$border-radius, $size) - 1px); + border-top-right-radius: calc(map.get(tokens.$border-radius, $size) - 1px); + + border-top-color: map.get(tokens.$border-color, dark); + border-left-color: map.get(tokens.$border-color, dark); + border-right-color: map.get(tokens.$border-color, dark); + @include light-mode { + border-top-color: map.get(tokens.$border-color, light); + border-left-color: map.get(tokens.$border-color, light); + border-right-color: map.get(tokens.$border-color, light); + } +} + +@mixin border-bottom($size) { + border-bottom-width: 1px; + border-bottom-style: solid; + border-left-style: solid; + border-right-style: solid; + border-left-width: 1px; + border-right-width: 1px; + border-bottom-left-radius: calc(map.get(tokens.$border-radius, $size) - 1px); + border-bottom-right-radius: calc(map.get(tokens.$border-radius, $size) - 1px); + + border-bottom-color: map.get(tokens.$border-color, dark); + border-left-color: map.get(tokens.$border-color, dark); + border-right-color: map.get(tokens.$border-color, dark); + @include light-mode { + border-bottom-color: map.get(tokens.$border-color, light); + border-left-color: map.get(tokens.$border-color, light); + border-right-color: map.get(tokens.$border-color, light); + } +} + +@mixin shadow { + box-shadow: + 0 0 #0000, + 0 0 #0000, + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px -1px rgba(0, 0, 0, 0.1); + @include light-mode { + box-shadow: + 0 0 #0000, + 0 0 #0000, + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px -1px rgba(0, 0, 0, 0.1); + } +} + @mixin transition($property, $duration) { transition-property: $property; transition-duration: map.get(tokens.$duration, $duration); } +@mixin margin-y($size) { + $spacing: map.get(tokens.$spacing, $size); + + margin: $spacing 0; +} + @mixin pad($size) { $padding: map.get(tokens.$padding, $size); diff --git a/src/sass/_tokens.scss b/src/sass/_tokens.scss index b9e9bce..20068dd 100644 --- a/src/sass/_tokens.scss +++ b/src/sass/_tokens.scss @@ -41,7 +41,7 @@ $colors: ( $code: ( // Default values taken from Shiki's 'classic' output type - bg-dark: #24292e, + bg-dark: #231f20, bg-light: #fff ); @@ -101,6 +101,8 @@ $spacing: ( xs: 0.25rem, sm: 0.5rem, base: 0.75rem, + lg: 0.75rem, + xl: 1rem, ); $breakpoints: (