diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..6e8d8c4 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,25 @@ +name: Create Release +on: + push: + tags: + - "*" +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install dependencies + run: yarn install + + - name: Compile assets + run: yarn build + + - name: Create block tarballs + run: node ./release.ts + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: dist/*.tar.gz diff --git a/.gitignore b/.gitignore index d451ff1..ef2fbc9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules dist dist-ssr *.local +.yalc \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/README.md b/README.md new file mode 100644 index 0000000..60972d9 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# GitHub Blocks Examples + +The [GitHub Blocks](https://github.com/githubnext/blocks) app depends on a set of "blocks" that handle how to render files and folders. This repo contains a set of example blocks that can serve as inspiration for you to create your own. + +## Example blocks + +Blocks come in two types: file blocks and folder blocks. + +All blocks require an object within [`blocks.config.json`](https://github.com/githubnext/blocks-examples/blob/main/blocks.config.json#L32) to describe their intended use. For example: + +```json +{ + "type": "file", + "id": "css-block", + "title": "Styleguide block", + "description": "View selectors in a css file", + "sandbox": false, + "entry": "blocks/file-blocks/css.tsx", + "matches": ["*.css"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/global.css" +} +``` + +> 👀 Preview these example blocks by going to [`blocks.githubnext.com`](https://blocks.githubnext.com/githubnext/blocks)! + +### File blocks + +| Block name | Description | Supported extensions | +| ---------- | ---------------------------- | -------------------- | +| Code | Simple block for code | all extensions | +| 3D Files | 3D model block with Three.js | gltf, glb | +| Css | View selectors in a css file | css | +| Excalidraw | A drawing/whiteboard block | excalidraw | +| Flat | A block for flat data files | csv, json | +| Html | Render html | html | + +### Folder blocks + +| Block name | Description | +| ---------- | ----------------------------------------- | +| Minimap | A visualization of your folders and files | + +## Create your own custom blocks + +Follow the instructions in our [custom blocks template](https://github.com/githubnext/blocks-template) repository. diff --git a/blocks.config.json b/blocks.config.json new file mode 100644 index 0000000..03d07f3 --- /dev/null +++ b/blocks.config.json @@ -0,0 +1,222 @@ +[ + { + "type": "file", + "id": "code-block", + "title": "Code", + "description": "Read & edit code", + "entry": "blocks/file-blocks/code/index.tsx", + "matches": ["*"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/Aside.jsx" + }, + { + "type": "file", + "id": "excalidraw", + "title": "Excalidraw diagram", + "description": "View and edit Excalidraw diagrams", + "entry": "blocks/file-blocks/excalidraw/index.tsx", + "matches": ["*.excalidraw"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/drawing.excalidraw" + }, + { + "type": "file", + "id": "html", + "title": "HTML", + "description": "View basic HTML content", + "entry": "blocks/file-blocks/html.tsx", + "matches": ["*.html", "*.svelte"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/photos.html" + }, + { + "type": "file", + "id": "css", + "title": "CSS Styleguide", + "description": "View CSS styles in an example styleguide", + "entry": "blocks/file-blocks/css.tsx", + "matches": ["*.css"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/global.css" + }, + { + "type": "file", + "id": "image", + "title": "Image", + "description": "View PNG, JPG, GIF, ICO and SVG images", + "entry": "blocks/file-blocks/image.tsx", + "matches": ["*.png", "*.ico", "*.jpg", "*.jpeg", "*.gif", "*.svg"], + "example_path": "https://github.com/pmndrs/react-spring/blob/HEAD/assets/projects/aragon.png?raw=true" + }, + { + "type": "file", + "id": "json", + "title": "JSON explorer", + "description": "An interactive view of JSON data", + "entry": "blocks/file-blocks/json.tsx", + "matches": [ + "*.json", + "*.webmanifest", + "webmanifest", + "prettierrc", + "*.prettierrc", + "*.yaml", + "*.yml" + ], + "example_path": "https://github.com/d3/d3-geo/blob/main/package.json" + }, + { + "type": "file", + "id": "geojson", + "title": "GeoJSON explorer", + "description": "View & edit GeoJSON data", + "entry": "blocks/file-blocks/geojson.tsx", + "matches": ["*.geo.json", "*.geojson"], + "example_path": "https://github.com/d3/d3-geo/blob/main/package.json" + }, + { + "type": "file", + "id": "3d-model", + "title": "3D model", + "description": "View 3D models", + "entry": "blocks/file-blocks/3d-files.tsx", + "matches": ["*.gltf", "*.glb"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/avocado.glb" + }, + { + "type": "file", + "id": "flat", + "title": "Spreadsheet", + "description": "View & edit CSV and YAML files in a spreadsheet", + "entry": "blocks/file-blocks/flat.tsx", + "matches": ["*.csv", "*.yml", "*.yaml"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/data.csv" + }, + { + "type": "file", + "id": "chart", + "title": "Chart", + "description": "Visualize CSV data in a few different chart types", + "entry": "blocks/file-blocks/charts/index.tsx", + "matches": ["*.csv"], + "example_path": "https://github.com/the-pudding/data/blob/master/pockets/measurements.csv" + }, + { + "type": "file", + "id": "markdown-block", + "title": "Markdown", + "description": "View and edit Markdown content, with the ability to embed other blocks", + "entry": "blocks/file-blocks/markdown-edit/index.tsx", + "matches": ["*.md", "*.markdown", "*.mdx", "*"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/README.md" + }, + { + "type": "file", + "id": "sandbox", + "title": "JS Sandbox", + "description": "Run JavaScript code in a sandbox", + "entry": "blocks/file-blocks/sandbox/index.tsx", + "matches": ["*.js", "*.ts", "*.tsx", "*.jsx"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/README.md" + }, + { + "type": "file", + "id": "react-feedback", + "title": "React component feedback", + "description": "View & give feedback on a React component", + "entry": "blocks/file-blocks/annotate-react/index.tsx", + "matches": ["*.jsx", "*.tsx"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/Aside.jsx" + }, + { + "type": "file", + "id": "sentence-encoder", + "title": "Sentence encoder", + "description": "Experiment with a sentence-encoder", + "entry": "blocks/file-blocks/sentence-encoder.tsx", + "matches": ["*.json"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/queries.json" + }, + { + "type": "file", + "id": "processing", + "title": "p5.js", + "description": "Run a p5.js sketch", + "sandbox": true, + "entry": "blocks/file-blocks/processing.tsx", + "matches": ["*.js"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/processing-sketch.js" + }, + { + "type": "file", + "id": "summarize", + "title": "Summarize", + "description": "Summarize parts of a file using ML", + "entry": "blocks/file-blocks/summarize/index.tsx", + "matches": ["*.js", "*.ts", "*.tsx", "*.jsx"], + "example_path": "https://github.com/mattdesl/canvas-sketch/blob/master/lib/save.js" + }, + { + "type": "file", + "id": "explain", + "title": "Explain", + "description": "Explain parts of a file using ML", + "entry": "blocks/file-blocks/explain/index.tsx", + "matches": ["*.js", "*.ts", "*.tsx", "*.jsx", "*.rb", "*.py"], + "example_path": "https://github.com/mattdesl/canvas-sketch/blob/master/lib/save.js" + }, + { + "type": "file", + "id": "edit", + "title": "Edit code with ML", + "description": "Edit code by prompting a Machine Learning model", + "entry": "blocks/file-blocks/edit/index.tsx", + "matches": ["*"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/processing-sketch.js" + }, + { + "type": "file", + "id": "edit-and-preview", + "title": "Edit with Preview", + "description": "Edit code side-by-side with a preview", + "entry": "blocks/file-blocks/edit-and-preview/index.tsx", + "matches": ["*"], + "example_path": "https://github.com/githubnext/blocks-tutorial/blob/main/README.md" + }, + { + "type": "folder", + "id": "minimap", + "title": "Minimap", + "description": "Visualize your folders and files in a minimap", + "entry": "blocks/folder-blocks/minimap/index.tsx", + "example_path": "https://github.com/githubnext/blocks-tutorial" + }, + { + "type": "folder", + "id": "overview", + "title": "Overview", + "description": "View an overview of a folder: including README, license, and recent activity", + "entry": "blocks/folder-blocks/overview/index.tsx", + "example_path": "https://github.com/githubnext/blocks-tutorial" + }, + { + "type": "folder", + "id": "dashboard", + "title": "Dashboard", + "description": "View other blocks in a dashboard view", + "entry": "blocks/folder-blocks/dashboard/index.tsx", + "example_path": "https://github.com/githubnext/blocks-tutorial" + }, + { + "type": "folder", + "id": "code-tour", + "title": "Code Tour", + "description": "Create documented tours of your code", + "entry": "blocks/folder-blocks/code-tour/index.tsx", + "example_path": "https://github.com/githubnext/blocks-tutorial" + }, + { + "type": "folder", + "id": "infinite-canvas", + "title": "Infinite Canvas", + "description": "View and annotate your files in an infinite canvas", + "entry": "blocks/folder-blocks/infinite-canvas/index.tsx", + "example_path": "https://github.com/githubnext/blocks-tutorial" + } +] diff --git a/blocks/file-blocks/3d-files.tsx b/blocks/file-blocks/3d-files.tsx new file mode 100644 index 0000000..bd64e17 --- /dev/null +++ b/blocks/file-blocks/3d-files.tsx @@ -0,0 +1,48 @@ +import { Suspense, useRef, useState, useEffect } from "react"; +import { tw } from "twind"; +import { PerspectiveCamera, useGLTF } from "@react-three/drei"; +import { Canvas, useLoader, useStore } from "@react-three/fiber"; +import { OrbitControls } from "@react-three/drei"; +import { FileBlockProps } from "@githubnext/blocks"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; + +const LControl = () => { + // @ts-ignore + const dom = useStore((state) => state.dom); + const control = useRef(null); + + // @ts-ignore + return ; +}; + +function Model({ url }: { url: string }) { + // need to load gltf model this way instead of with hook + // or you get an infinite loop on the production sandbox side + const { scene } = useLoader(GLTFLoader, url); + return ; +} + +export default function (props: FileBlockProps) { + const { context } = props; + + const url = `https://raw.githubusercontent.com/${context.owner}/${context.repo}/${context.sha}/${context.path}`; + + return ( + + {/* + // @ts-ignore */} + + + + + + + + + ); +} diff --git a/blocks/file-blocks/annotate-react/CodeSandbox.tsx b/blocks/file-blocks/annotate-react/CodeSandbox.tsx new file mode 100644 index 0000000..38073ff --- /dev/null +++ b/blocks/file-blocks/annotate-react/CodeSandbox.tsx @@ -0,0 +1,57 @@ +import { SandpackPreview, SandpackProvider } from "@codesandbox/sandpack-react"; +import { useMemo } from "react"; +import "./style.css"; + +export const CodeSandbox = ({ + children, + dependencies, +}: { + children: string; + dependencies?: string[]; +}) => { + const files = useMemo( + () => ({ + "/src/index.js": { code: children }, + "/src/styles.css": { code: "" }, + }), + [children] + ); + + const parsedDependencies = useMemo( + () => parseDependencies(dependencies || []), + [dependencies] + ); + + return ( +
+ + + +
+ ); +}; + +const parseDependencies = (dependencies: string[]): Record => { + let res = {}; + dependencies.forEach((dep) => { + const [name, version = "latest"] = dep.split("@"); + // @ts-ignore + res[name] = version; + }); + return res; +}; diff --git a/blocks/file-blocks/annotate-react/index.tsx b/blocks/file-blocks/annotate-react/index.tsx new file mode 100644 index 0000000..60a866e --- /dev/null +++ b/blocks/file-blocks/annotate-react/index.tsx @@ -0,0 +1,326 @@ +// @ts-ignore +import { tw } from "twind"; +import { FileBlockProps } from "@githubnext/blocks"; +import { useState } from "react"; +// @ts-ignore +import Annotation from "react-image-annotation"; +import { + OvalSelector, + PointSelector, + RectangleSelector, + // @ts-ignore +} from "react-image-annotation/lib/selectors"; +// @ts-ignore +import { CodeSandbox } from "./CodeSandbox.tsx"; +import { + Button, + FormControl, + Radio, + RadioGroup, + Textarea, + TextInput, + ActionList, +} from "@primer/react"; +import { PlusIcon, TrashIcon } from "@primer/octicons-react"; + +export default function ({ + content, + context, + metadata, + onUpdateMetadata, +}: FileBlockProps) { + const { path } = context; + + const componentName = path.split("/").pop()?.split(".")[0]; + const [componentDefinition, setComponentDefinition] = useState( + `<${componentName}>\n` + ); + const [annotations, setAnnotations] = useState([]); + + const wrappedContents = + typeof document === "undefined" + ? "" + : ` +import ReactDOM from "react-dom/client"; + + ${content} + +// render element +const container = document.createElement("div"); +document.body.appendChild(container); +const root = ReactDOM.createRoot(container) +root.render( + ${componentDefinition || `<${componentName} />`} +) +`; + + return ( +
+
+ + + {wrappedContents} + + +
+ +
+ ); +} + +const annotationTypes = [ + { + id: RectangleSelector.TYPE, + name: "Rectangle", + }, + { + id: PointSelector.TYPE, + name: "Point", + }, + { + id: OvalSelector.TYPE, + name: "Oval", + }, +]; + +type AnnotationType = { + geometry: string; + data: { + id: number; + }; +}; +type AnnotationSet = { + annotations: AnnotationType[]; + title: string; + componentDefinition: string; +}; + +const Annotator = ({ + annotations, + setAnnotations, + children, +}: { + annotations: AnnotationType[]; + setAnnotations: (annotations: AnnotationType[]) => void; + children: any; +}) => { + const [annotation, setAnnotation] = useState({}); + const [annotationType, setAnnotationType] = useState(annotationTypes[0].id); + + const onAddAnnotation = (annotation: AnnotationType) => { + const { geometry, data } = annotation; + setAnnotation({}); + const newAnnotations = [ + ...annotations, + { + geometry, + data: { + ...data, + id: Math.random(), + }, + }, + ]; + setAnnotations(newAnnotations); + }; + + return ( +
+
+
+ + Annotation Type + {annotationTypes.map(({ id, name }) => ( + + setAnnotationType(id)} + value={id} + /> + {name} + + ))} + +
+ +
+ +
+ {children} +
+
+
+
+
+ ); +}; + +const AnnotationSetList = ({ + saved, + onUpdateMetadata, + annotations, + setAnnotations, + componentName, + componentDefinition, + setComponentDefinition, +}: { + saved: AnnotationSet[]; + onUpdateMetadata: (metadata: any) => void; + annotations: AnnotationType[]; + componentName: string; + setAnnotations: (annotations: AnnotationType[]) => void; + componentDefinition: string; + setComponentDefinition: (componentDefinition: string) => void; +}) => { + const [title, setTitle] = useState(""); + + const canSubmitForm = !!annotations.length && title.length; + const selectedAnnotationSetString = annotationSetToString({ + title, + componentDefinition, + annotations, + }); + const selectedAnnotationSetIndex = saved.findIndex( + (set) => annotationSetToString(set) === selectedAnnotationSetString + ); + + return ( +
+
{ + e.preventDefault(); + if (!canSubmitForm) return; + const newMetadata = { + saved: [ + ...saved, + { + title, + componentDefinition, + annotations, + }, + ], + }; + onUpdateMetadata(newMetadata); + }} + > +
+ + Annotation Set Title + setTitle(e.target.value)} + /> + +
+
+ + Component Definition + + You can specify the props and children + +