diff --git a/.changeset/rude-hotels-dress.md b/.changeset/rude-hotels-dress.md new file mode 100644 index 0000000..e64c906 --- /dev/null +++ b/.changeset/rude-hotels-dress.md @@ -0,0 +1,6 @@ +--- +'swingset': minor +'swingset-theme-hashicorp': minor +--- + +New swingset! diff --git a/.gitignore b/.gitignore index e8a270e..3e112dd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ node_modules .next .yalc yalc.lock +.vscode + +# Turbo +*/**/.turbo + +# Vercel +.vercel diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e2eeb9f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Developing swingset + +## Setup + +Install dependencies: + +```shell-session +npm install +``` + +## Running the example app + +First, run the `dev` task for `example-basic`'s dependencies: + +```shell-session +npx turbo run dev --filter example-basic^... +``` + +Once the log output stops, we can stop the above process and run all of the dev tasks: + +```shell-session +npx turbo run dev --filter example-basic... +``` + +Finally, visit in your browser. Any changes to the core `swingset` package will be rebuilt and reflect in the running example app. diff --git a/__swingset_data.ts b/__swingset_data.ts deleted file mode 100644 index 1e7d90e..0000000 --- a/__swingset_data.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { ComponentData, FormattedFileEntry, SwingsetData } from './types' - -// this import is always intercepted by a loader - -export const components: Record = {} -export const docs: Record = {} diff --git a/components-loader.js b/components-loader.js deleted file mode 100644 index 57854b6..0000000 --- a/components-loader.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -const globby = require('globby') -const fs = require('fs') -const path = require('path') -const { existsSync } = require('fsexists') -const matter = require('gray-matter') -const { getOptions } = require('loader-utils') -const slugify = require('slugify') - -module.exports = function swingsetComponentsLoader() { - const { pluginOptions, webpackConfig } = getOptions(this) - - // Resolve components glob - const allComponents = globby - .sync(`${pluginOptions.componentsRoot}`, { - onlyFiles: false, - }) - .filter((folderPath) => { - return !folderPath.includes('node_modules') - }) - - const usedComponents = removeComponentsWithoutDocs( - allComponents, - pluginOptions - ) - - // add components directory as a webpack dependency - this.addContextDependency(pluginOptions.componentsRoot.split('*')[0]) - const componentsWithNames = formatComponentsWithNames(usedComponents) - - // Resolve docs glob - const allDocs = globby.sync(`${pluginOptions.docsRoot}`) - const docsWithNames = formatDocsFilesWithNames(allDocs) - - return generateMetadataFile(componentsWithNames, docsWithNames) -} - -/** - * Go through each component and remove any components folders that don't have - * a "docs.mdx" file, since we don't need to display them. If a component exists - * without a docs page, and verbose mode is active, log a warning. - * @param {string[]} components - * @param {import('./types').PluginOptions} pluginOptions - * @returns {string[]} - */ -function removeComponentsWithoutDocs(components, pluginOptions) { - return components.reduce((memo, componentDir) => { - if (existsSync(path.join(componentDir, 'docs.mdx'))) { - memo.push(componentDir) - } else { - pluginOptions.verbose && - console.warn( - `The component "${componentDir}" does not have a "docs.mdx" file and therefore will not be documented.` - ) - } - return memo - }, []) -} - -/** - * Read the docs file name from the docs file, return the name and path. - * If the docs file doesn't have a name, throw a clear error. - * The format ends up like this: [{ name: 'Test', path: '/path/to/component' }] - * @param {string[]} docs - * @returns {import('./types').FormattedFileEntry[]} - */ -function formatDocsFilesWithNames(docs) { - return docs.map((docsFile) => { - const fileContent = fs.readFileSync(docsFile, 'utf8') - const { data } = matter(fileContent) - if (!data.name) { - throw new Error( - `The docs file at "${docsFile}" is missing metadata. Please add a human-readable name to display in the sidebar as "name" to the front matter at the top of the file.` - ) - } - return { - name: data.name, - path: docsFile, - slug: path.basename(docsFile, path.extname(docsFile)), - data, - } - }) -} - -/** - * Read the component name from the docs file, return the name and path. - * If the docs file doesn't have a name, throw a clear error. - * The format ends up like this: [{ name: 'Test', path: '/path/to/component' }] - * @param {string[]} components - * @returns {import('./types').FormattedFileEntry[]} - */ -function formatComponentsWithNames(components) { - return components.map((componentDir) => { - const docsFileContent = fs.readFileSync( - path.join(componentDir, 'docs.mdx'), - 'utf8' - ) - const { data } = matter(docsFileContent) - if (!data.componentName) { - throw new Error( - `The docs file at "${componentDir}" is missing metadata. Please add the component's name as you would like it to be imported as "componentName" to the front matter at the top of the file.` - ) - } - return { - name: data.componentName, - path: componentDir, - slug: slugify(data.componentName, { lower: true }), - data, - } - }) -} - -/** - * - Write out the component metadata to a file, which is formatted as such: - ``` - import ComponentName from '/absolute/path/to/component' - - export default { - ComponentName: { - path: '/absolute/path/to/component', - docsPath: '/absolute/path/to/component/docs.mdx', - propsPath: '/absolute/path/to/component/props.js', - slug: 'componentname', - exports: ComponentNameExports, - data: { componentName: 'ComponentName' } - }, - ... - } - ``` - * @param {import('./types').FormattedFileEntry[]} components - * @param {import('./types').FormattedFileEntry[]} docsFiles - * @returns - */ -function generateMetadataFile(components, docsFiles) { - const imports = components.reduce((memo, component) => { - memo += `import * as ${component.name}Exports from '${component.path}'\n` - return memo - }, '') - - const componentsData = components.reduce((acc, component) => { - // We can't just stringify here, because we need eg - // src: Button, <<< Button NOT in quotes - acc += ` '${component.name}': { - path: '${component.path}', - docsPath: '${path.join(component.path, 'docs.mdx')}', - propsPath: '${path.join(component.path, 'props.js')}', - slug: '${component.slug}', - exports: ${component.name}Exports, - data: ${JSON.stringify(component.data, null, 2)} - }, - ` - return acc - }, '') - - const docsData = docsFiles.reduce((acc, docsEntry) => { - acc[docsEntry.slug] = docsEntry - return acc - }, {}) - - let contents = '' - contents += imports + '\n' - contents += `export const components = {\n${componentsData}\n}\n` - contents += `export const docs = ${JSON.stringify(docsData, null, 2)}\n` - - return contents -} diff --git a/components/knobs-component/index.tsx b/components/knobs-component/index.tsx deleted file mode 100644 index 0d015eb..0000000 --- a/components/knobs-component/index.tsx +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import sg from '../shared.module.css' -import s from './style.module.css' -import classnames from 'classnames' -import React, { useState } from 'react' -import { useRestoreUrlState, setUrlState } from '../../utils/url-state' -import createId from '../../utils/create-id' -import scrollToElement from '../../utils/scroll-to-element' -import { Knob, Knobs } from '../../types' - -export default function createKnobsComponent( - scope: Record -) { - const name = Object.keys(scope)[0] - const Component = Object.values(scope)[0] - - return function KnobsComponent({ knobs }: { knobs: Knobs }) { - const id = createId(knobs) - const [values, setValues] = useState(knobs) - - // if there's url state and it applies to this element, restore it - useRestoreUrlState((qs) => { - if (qs.id == id) { - setValues(qs.values) - scrollToElement(id) - } - }) - - return ( -
-
setUrlState(id, values, true)}> - Share -
- -
-
Controls
- {renderControls(values, setValues)} -
-
- ) - } -} - -function isKnob(v: any): v is Knob { - return typeof v === 'object' && v.control -} - -function renderControls( - values: Knobs, - setValues: (arg: Knobs) => void, - indentLevel = 0 -) { - const [fieldErrors, setFieldErrors] = useState>({}) - - return Object.keys(values).map((k) => { - const valuesCopy = JSON.parse(JSON.stringify(values)) - const v = values[k] - let control - - if (isKnob(v)) { - if (v.control.type === 'text') { - control = ( - { - valuesCopy[k].control.value = e.target.value - setValues(valuesCopy) - }} - /> - ) - } - - if (v.control.type === 'select') { - control = ( - - ) - } - - if (v.control.type === 'checkbox') { - control = ( - { - valuesCopy[k].control.value = e.target.checked - setValues(valuesCopy) - }} - /> - ) - } - - if (v.control.type === 'json') { - control = ( -
-