diff --git a/.eslintignore b/.eslintignore index 4d6880d..070fbf6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ dist examples +www diff --git a/README.md b/README.md index 986c572..d1e3450 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ types and how to resolve them. - [`@field`](#field) - [`@implements`](#implements) - [`@discriminates`](#discriminates) - - [`@discriminationAlias`](#discriminationalias) - [`@resolve`](#resolve) - [Getting started](#getting-started) - [GraphQL Application](#graphql-application) @@ -122,6 +121,8 @@ type Service @implements(interface: "Entity") { _NOTE: In this example if we have data of `Entity` type and it has `kind` field_ _with `Component` value, that means data will be resolved to `Component` type_ +#### `opaqueType` + There is a special case when your runtime data doesn't have a value that can be used to discriminate the interface or there is no type that matches the value. In this case, you can define `opaqueType` argument @@ -141,26 +142,25 @@ plugin will generate it for you. There is another way to define opaque types for all interfaces by using `generateOpaqueTypes` option for GraphQL plugin. -### `@discriminationAlias` +#### `aliases` -By default value from `with` argument is used to find a type as-is or converted to PascalCase. -Sometimes you need to match the value with a type that has a different name. -In this case, you can use `@discriminationAlias` directive. +By default value from `with` argument is used to find a type as-is or converted to PascalCase. +Sometimes you need to match the value with a type that has a different name. +In this case, you can define `aliases` argument. ```graphql interface API @implements(interface: "Node") - @discriminates(with: "spec.type") - @discriminationAlias(value: "openapi", type: "OpenAPI") { + @discriminates(with: "spec.type", aliases: [{ value: "grpc", type: "GrpcAPI" }]) { # ... } -type OpenAPI @implements(interface: "API") { +type GrpcAPI @implements(interface: "API") { # ... } ``` -This means, when `spec.type` equals to `openapi`, the `API` interface will be resolved to `OpenAPI` type. +This means, when `spec.type` equals to `grpc`, the `API` interface will be resolved to `GrpcAPI` type. ### `@resolve` diff --git a/src/__snapshots__/schema.graphql.snap b/src/__snapshots__/schema.graphql.snap index 645e3ae..d753e93 100644 --- a/src/__snapshots__/schema.graphql.snap +++ b/src/__snapshots__/schema.graphql.snap @@ -1,5 +1,6 @@ -directive @discriminates(opaqueType: String, with: _DirectiveArgument_) on INTERFACE +directive @discriminates(aliases: [DiscriminationAlias!], opaqueType: String, with: _DirectiveArgument_) on INTERFACE +"""@deprecated Please use `@discriminates(aliases: [...])`""" directive @discriminationAlias(type: String!, value: String!) repeatable on INTERFACE directive @field(at: _DirectiveArgument_, default: _DirectiveArgument_) on FIELD_DEFINITION @@ -14,6 +15,11 @@ interface Connection { pageInfo: PageInfo! } +input DiscriminationAlias { + type: String! + value: String! +} + interface Edge { cursor: String! node: Node! diff --git a/src/__snapshots__/types.ts.snap b/src/__snapshots__/types.ts.snap index 4c6f045..358754e 100644 --- a/src/__snapshots__/types.ts.snap +++ b/src/__snapshots__/types.ts.snap @@ -25,6 +25,11 @@ export type Connection = { pageInfo: PageInfo; }; +export type DiscriminationAlias = { + type: Scalars['String']['input']; + value: Scalars['String']['input']; +}; + export type Edge = { cursor: Scalars['String']['output']; node: Node; @@ -137,6 +142,7 @@ export type ResolversInterfaceTypes> = { export type ResolversTypes = { Boolean: ResolverTypeWrapper; Connection: ResolverTypeWrapper['Connection']>; + DiscriminationAlias: DiscriminationAlias; Edge: ResolverTypeWrapper['Edge']>; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; @@ -151,6 +157,7 @@ export type ResolversTypes = { export type ResolversParentTypes = { Boolean: Scalars['Boolean']['output']; Connection: ResolversInterfaceTypes['Connection']; + DiscriminationAlias: DiscriminationAlias; Edge: ResolversInterfaceTypes['Edge']; ID: Scalars['ID']['output']; Int: Scalars['Int']['output']; @@ -162,6 +169,7 @@ export type ResolversParentTypes = { }; export type DiscriminatesDirectiveArgs = { + aliases?: Maybe>; opaqueType?: Maybe; with?: Maybe; }; diff --git a/src/core/core.graphql b/src/core/core.graphql index 04f4b13..b27d34f 100644 --- a/src/core/core.graphql +++ b/src/core/core.graphql @@ -5,7 +5,11 @@ directive @field( directive @discriminates( with: _DirectiveArgument_ opaqueType: String + aliases: [DiscriminationAlias!] ) on INTERFACE +""" +@deprecated Please use `@discriminates(aliases: [...])` +""" directive @discriminationAlias( value: String! type: String! @@ -15,6 +19,11 @@ directive @resolve(at: _DirectiveArgument_, nodeType: String, from: String) on F scalar _DirectiveArgument_ +input DiscriminationAlias { + value: String! + type: String! +} + interface Node { id: ID! } diff --git a/src/core/resolveDirectiveMapper.ts b/src/core/resolveDirectiveMapper.ts index 36cf4ef..92874e8 100644 --- a/src/core/resolveDirectiveMapper.ts +++ b/src/core/resolveDirectiveMapper.ts @@ -1,17 +1,13 @@ -import _ from "lodash"; -import { connectionFromArray, ConnectionArguments } from "graphql-relay"; import { GraphQLInputObjectType, - type GraphQLFieldConfig, - type GraphQLInterfaceType, GraphQLInt, GraphQLString, + type GraphQLFieldConfig, + type GraphQLInterfaceType, } from "graphql"; -import type { - DirectiveMapperAPI, - FieldResolver, - ResolverContext, -} from "../types.js"; +import { ConnectionArguments, connectionFromArray } from "graphql-relay"; +import _ from "lodash"; +import { HYDRAPHQL_EXTENSION } from "src/constants.js"; import { createConnectionType, decodeId, @@ -21,7 +17,11 @@ import { isNamedListType, unboxNamedType, } from "../helpers.js"; -import { HYDRAPHQL_EXTENSION } from "src/constants.js"; +import type { + DirectiveMapperAPI, + FieldResolver, + ResolverContext, +} from "../types.js"; export function resolveDirectiveMapper( fieldName: string, @@ -97,6 +97,11 @@ export function resolveDirectiveMapper( } } + // FIXME: This doesn't work if a single ref is resolved to multiple nodes + // We need to load all nodes + // So here we might have a single connection or array of connections + // TODO Throw an error if we have an array of refs and a single connection + // TODO Throw an error if we have a single ref and an array of connections const ids = ((ref ?? []) as string[]).map((r) => ({ id: encodeId({ source, @@ -108,6 +113,7 @@ export function resolveDirectiveMapper( }), })); + // FIXME: We need to apply connection return { ...connectionFromArray(ids, args as ConnectionArguments), count: ids.length, @@ -176,6 +182,7 @@ export function resolveDirectiveMapper( fieldResolver, }, }; + // TODO Add test case of handling [Connection] type (array of connections) field.resolve = async ({ id }, args, context, info) => { if (directive.at === "id") return { id }; diff --git a/src/index.ts b/src/index.ts index e8ca36c..b176d11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from "./createLoader.js"; export * from "./createGraphQLApp.js"; export * from "./core/core.js"; export * from "./helpers.js"; +export * from "./loadSchema.js"; export { transformSchema } from "./transformSchema.js"; export type { GraphQLContext, diff --git a/src/loadSchema.ts b/src/loadSchema.ts index daea71c..7b59ce5 100644 --- a/src/loadSchema.ts +++ b/src/loadSchema.ts @@ -1,17 +1,19 @@ import { CodeFileLoader } from "@graphql-tools/code-file-loader"; import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"; -import { loadTypedefs } from "@graphql-tools/load"; +import { loadTypedefs, loadTypedefsSync } from "@graphql-tools/load"; import { getResolversFromSchema, printSchemaWithDirectives, + Source, } from "@graphql-tools/utils"; import { createModule, gql } from "graphql-modules"; -export async function loadSchema(schema: string | string[]) { - const sources = await loadTypedefs(schema, { - sort: true, - loaders: [new CodeFileLoader(), new GraphQLFileLoader()], - }); +const loadTypeDefsOptions = { + sort: true, + loaders: [new CodeFileLoader(), new GraphQLFileLoader()], +}; + +function sources2modules(sources: Source[]) { return sources.map((source, index) => createModule({ id: source.location ?? `unknown_${index}`, @@ -24,3 +26,11 @@ export async function loadSchema(schema: string | string[]) { }), ); } + +export async function loadSchema(schema: string | string[]) { + return sources2modules(await loadTypedefs(schema, loadTypeDefsOptions)); +} + +export function loadSchemaSync(schema: string | string[]) { + return sources2modules(loadTypedefsSync(schema, loadTypeDefsOptions)); +} diff --git a/src/mapDirectives.test.ts b/src/mapDirectives.test.ts index 626187a..5eb8d09 100644 --- a/src/mapDirectives.test.ts +++ b/src/mapDirectives.test.ts @@ -62,10 +62,14 @@ describe("mapDirectives", () => { const schema = transform(gql` interface Entity @implements(interface: "Node") - @discriminates(with: "kind") - @discriminationAlias(value: "component", type: "Component") - @discriminationAlias(value: "template", type: "Template") - @discriminationAlias(value: "location", type: "Location") { + @discriminates( + with: "kind" + aliases: [ + { value: "component", type: "Component" } + { value: "template", type: "Template" } + { value: "location", type: "Location" } + ] + ) { totalCount: Int! } @@ -390,14 +394,18 @@ describe("mapDirectives", () => { ); }); - void test(`should fail if @discriminationAlias has ambiguous types`, () => { + void test(`should fail if discrimination aliases have ambiguous types`, () => { expect(() => transform(gql` interface Entity @implements(interface: "Node") - @discriminates(with: "kind") - @discriminationAlias(value: "component", type: "EntityComponent") - @discriminationAlias(value: "component", type: "Component") { + @discriminates( + with: "kind" + aliases: [ + { value: "component", type: "EntityComponent" } + { value: "component", type: "Component" } + ] + ) { name: String! } @@ -414,20 +422,6 @@ describe("mapDirectives", () => { ); }); - void test(`should fail if @discriminationAlias is used without @discriminates`, () => { - expect(() => - transform(gql` - interface Entity - @implements(interface: "Node") - @discriminationAlias(value: "component", type: "EntityComponent") { - name: String! - } - `), - ).toThrow( - `The "Entity" interface has @discriminationAlias directive but doesn't have @discriminates directive`, - ); - }); - void test(`should fail if interface has multiple implementations and @discriminates is not specified`, () => { expect(() => transform(gql` @@ -565,9 +559,11 @@ describe("mapDirectives", () => { expect(() => transform(gql` interface Entity - @discriminates(with: "kind") - @implements(interface: "Node") - @discriminationAlias(value: "component", type: "Component") { + @discriminates( + with: "kind" + aliases: [{ value: "component", type: "Component" }] + ) + @implements(interface: "Node") { name: String! } type Resource @implements(interface: "Entity") { @@ -580,7 +576,7 @@ describe("mapDirectives", () => { } `), ).toThrow( - 'Type(-s) "Component" in `interface Entity @discriminationAlias(value: ..., type: ...)` must implement "Entity" interface by using @implements directive', + 'Type(-s) "Component" in `interface Entity @discriminates(aliases: [...])` must implement "Entity" interface by using @implements directive', ); }); @@ -895,9 +891,13 @@ describe("mapDirectives", () => { id: "test", typeDefs: gql` interface Node - @discriminates(with: "__source") - @discriminationAlias(value: "Mock", type: "Entity") - @discriminationAlias(value: "GraphQL", type: "GraphQLEntity") + @discriminates( + with: "__source" + aliases: [ + { value: "Mock", type: "Entity" } + { value: "GraphQL", type: "GraphQLEntity" } + ] + ) type Entity @implements(interface: "Node") { parent: GraphQLEntity @resolve(at: "spec.parentId", from: "GraphQL") @@ -965,9 +965,13 @@ describe("mapDirectives", () => { id: "test", typeDefs: gql` interface Node - @discriminates(with: "__source") - @discriminationAlias(value: "Mock", type: "Entity") - @discriminationAlias(value: "Tasks", type: "TaskProperty") + @discriminates( + with: "__source" + aliases: [ + { value: "Mock", type: "Entity" } + { value: "Tasks", type: "TaskProperty" } + ] + ) type Entity @implements(interface: "Node") { property(name: String!): TaskProperty @@ -1051,9 +1055,13 @@ describe("mapDirectives", () => { id: "test", typeDefs: gql` interface Node - @discriminates(with: "__source") - @discriminationAlias(value: "Mock", type: "Entity") - @discriminationAlias(value: "Tasks", type: "Task") + @discriminates( + with: "__source" + aliases: [ + { value: "Mock", type: "Entity" } + { value: "Tasks", type: "Task" } + ] + ) type Entity @implements(interface: "Node") { task(taskId: ID!): Task @resolve(from: "Tasks") @@ -1202,8 +1210,10 @@ describe("mapDirectives", () => { typeDefs: gql` interface Entity @implements(interface: "Node") - @discriminates(with: "kind") - @discriminationAlias(value: "User", type: "Employee") { + @discriminates( + with: "kind" + aliases: [{ value: "User", type: "Employee" }] + ) { name: String! @field(at: "name") } diff --git a/src/mapInterfaceType.ts b/src/mapInterfaceType.ts index 4f8c7fe..5540289 100644 --- a/src/mapInterfaceType.ts +++ b/src/mapInterfaceType.ts @@ -1,5 +1,8 @@ -import _ from "lodash"; -import { pascalCase } from "pascal-case"; +import type { + GraphQLFieldConfigMap, + GraphQLNamedType, + GraphQLTypeResolver, +} from "graphql"; import { GraphQLInterfaceType, GraphQLObjectType, @@ -7,17 +10,15 @@ import { isObjectType, isUnionType, } from "graphql"; -import type { - GraphQLFieldConfigMap, - GraphQLNamedType, - GraphQLTypeResolver, -} from "graphql"; +import _ from "lodash"; +import { pascalCase } from "pascal-case"; +import { decodeId } from "./helpers.js"; import type { DirectiveMapperAPI, + DiscriminationArgs, NamedType, ResolverContext, } from "./types.js"; -import { decodeId } from "./helpers.js"; function isRelatedType( resolvedType: GraphQLObjectType, @@ -33,8 +34,9 @@ function isRelatedType( function validateDiscriminatesDirective( interfaceName: string, - directive: Record | undefined, - aliases: { value: string; type: string }[], + directive: DiscriminationArgs | undefined, + aliases: Map, + ambiguousAliases: Map, api: DirectiveMapperAPI, { implementationsMap, @@ -56,9 +58,9 @@ function validateDiscriminatesDirective( ); } if (!directive) { - if (aliases.length > 0) { + if (aliases.size > 0) { throw new Error( - `The "${interfaceName}" interface has @discriminationAlias directive but doesn't have @discriminates directive`, + `The "${interfaceName}" interface has discrimination aliases but doesn't have @discriminates directive`, ); } if (discriminates && discriminates.size > 1) { @@ -74,7 +76,7 @@ function validateDiscriminatesDirective( ); } - const opaqueType = directive.opaqueType as string | undefined; + const opaqueType = directive.opaqueType; const opaqueTypename = opaqueType ?? (generateOpaqueTypes ? `Opaque${interfaceName}` : undefined); @@ -130,26 +132,18 @@ function validateDiscriminatesDirective( } } - const aliasesMap = aliases.reduce( - (map, alias) => ({ - ...map, - [alias.value]: [...(map[alias.value] ?? []), alias.type], - }), - {} as Record, - ); - const ambiguousAliases = Object.entries(aliasesMap).filter( - ([, types]) => types.length > 1, - ); - if (ambiguousAliases.length) { + if (ambiguousAliases.size) { throw new Error( - `The following discrimination aliases are ambiguous: ${ambiguousAliases + `The following discrimination aliases are ambiguous: ${[ + ...ambiguousAliases.entries(), + ] .map(([alias, types]) => `"${alias}" => "${types.join('" | "')}"`) .join(", ")}`, ); } - const types = Object.values(aliasesMap).map( - ([type]) => [type, api.typeMap[type]] as const, + const types = [...aliases.values()].map( + (type) => [type, api.typeMap[type]] as const, ); const invalidTypes = types .filter(([, type]) => type && !isObjectType(type) && !isInterfaceType(type)) @@ -162,7 +156,7 @@ function validateDiscriminatesDirective( throw new Error( `Type(-s) "${invalidTypes.join( '", "', - )}" in \`interface ${interfaceName} @discriminationAlias(value: ..., type: ...)\` are not object types or interfaces`, + )}" in \`interface ${interfaceName} @discriminates(aliases: [...])\` are not object types or interfaces`, ); } if (typesWithWrongInterfaces.length) { @@ -171,15 +165,15 @@ function validateDiscriminatesDirective( .map(([name]) => name) .join( '", "', - )}" in \`interface ${interfaceName} @discriminationAlias(value: ..., type: ...)\` must implement "${interfaceName}" interface by using @implements directive`, + )}" in \`interface ${interfaceName} @discriminates(aliases: [...])\` must implement "${interfaceName}" interface by using @implements directive`, ); } } function defineResolver( interfaceName: string, - directive: Record | undefined, - aliases: { value: string; type: string }[], + directive: DiscriminationArgs | undefined, + aliases: Map, { implementationsMap, generateOpaqueTypes, @@ -194,7 +188,7 @@ function defineResolver( ? `Opaque${interfaceName}` : undefined; const opaqueTypeName = directive - ? (directive.opaqueType as string | undefined) ?? generatedOpaqueType + ? directive.opaqueType ?? generatedOpaqueType : implementationType; return async (source, context, info) => { @@ -222,8 +216,7 @@ function defineResolver( )}\` value which was discriminated by ${interfaceName} interface must be a string`, ); } - const typename = - aliases.find((alias) => alias.value === value)?.type ?? value; + const typename = aliases.get(value) ?? value; const type = schema.getType(typename) ?? schema.getType(pascalCase(typename)); @@ -309,7 +302,7 @@ export function mapInterfaceType( const [discriminatesDirective] = (api.getDirective( interfaceType, "discriminates", - ) ?? []) as (Record | undefined)[]; + ) ?? []) as (DiscriminationArgs | undefined)[]; const discriminationAliases = (api.getDirective( interfaceType, "discriminationAlias", @@ -323,17 +316,42 @@ export function mapInterfaceType( `The "resolveType" function has already been implemented for "${interfaceName}" interface which may lead to undefined behavior`, ); } + + if (discriminationAliases.length > 0) { + console.log( + '`@discriminationAlias` directive is deprecated, please use `@directive(with: "...", aliases: [{ value: "...", type: "..." }])`', + ); + } + + const ambiguousAliases = new Map(); + const aliases = new Map(); + const rawAliases = [ + ...discriminationAliases, + ...(discriminatesDirective?.aliases ?? []), + ]; + for (const alias of rawAliases) { + const existingAliasType = aliases.get(alias.value); + if (existingAliasType) { + ambiguousAliases.set(alias.value, [ + ...(ambiguousAliases.get(alias.value) ?? [existingAliasType]), + alias.type, + ]); + } + aliases.set(alias.value, alias.type); + } + validateDiscriminatesDirective( interfaceName, discriminatesDirective, - discriminationAliases, + aliases, + ambiguousAliases, api, options, ); const resolver = defineResolver( interfaceName, discriminatesDirective, - discriminationAliases, + aliases, options, ); @@ -369,8 +387,7 @@ export function mapInterfaceType( extensionASTNodes, }); - discriminationAliases - .map((alias) => alias.type) + [...aliases.values()] .filter((typename) => !(typename in api.typeMap)) .forEach((typename) => { api.typeMap[typename] = new GraphQLObjectType({ @@ -384,7 +401,7 @@ export function mapInterfaceType( }); const opaqueTypeName = - (discriminatesDirective?.opaqueType as string | undefined) ?? + discriminatesDirective?.opaqueType ?? (options.generateOpaqueTypes ? `Opaque${interfaceName}` : undefined); const { discriminates } = options.implementationsMap.get(interfaceName) ?? {}; if ( diff --git a/src/transformSchema.ts b/src/transformSchema.ts index e354e16..4051f6b 100644 --- a/src/transformSchema.ts +++ b/src/transformSchema.ts @@ -1,18 +1,20 @@ -import { Kind } from "graphql"; -import type { DocumentNode } from "graphql"; import { makeExecutableSchema } from "@graphql-tools/schema"; -import { validateSchema } from "graphql"; +import type { DocumentNode } from "graphql"; +import { Kind, validateSchema } from "graphql"; import type { Module, Resolvers } from "graphql-modules"; +import { CoreSync } from "./index.js"; +import { loadSchemaSync } from "./loadSchema.js"; import { mapDirectives } from "./mapDirectives.js"; import type { FieldDirectiveMapper, GraphQLModule } from "./types.js"; -import { CoreSync } from "./index.js"; export function transformSchema( - additionalModules: (GraphQLModule | Module)[] = [], + additionalModules: (GraphQLModule | Module | string)[] = [], { generateOpaqueTypes }: { generateOpaqueTypes?: boolean } = {}, ) { const postTransformers: GraphQLModule["postTransform"][] = []; - const modules = [CoreSync(), ...additionalModules]; + const modules = [CoreSync(), ...additionalModules].flatMap((m) => + typeof m === "string" ? loadSchemaSync(m) : m, + ); const directiveMappers: Record = {}; const typeDefs: DocumentNode[] = modules.flatMap((m) => { const { diff --git a/src/types.ts b/src/types.ts index 783b0ac..f1c982a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -80,3 +80,9 @@ export type FieldResolver = NonNullable< export interface FieldExtensions { fieldResolver: FieldResolver; } + +export interface DiscriminationArgs { + with?: string | string[]; + opaqueType?: string; + aliases?: { value: string; type: string }[]; +} diff --git a/tsconfig.json b/tsconfig.json index eb8c542..b4debfb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,5 +22,5 @@ "allowJs": true, "checkJs": true }, - "exclude": ["./dist", "./examples"] + "exclude": ["./dist", "./examples", "./www"] } diff --git a/www/.vscode/settings.json b/www/.vscode/settings.json new file mode 100644 index 0000000..cbac569 --- /dev/null +++ b/www/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} diff --git a/www/README.md b/www/README.md new file mode 100644 index 0000000..973fa92 --- /dev/null +++ b/www/README.md @@ -0,0 +1,7 @@ +## HydraphQL Website + +### Development + +```shellsession +$ deno task dev +``` diff --git a/www/assets/images/effection-logo.svg b/www/assets/images/effection-logo.svg new file mode 100644 index 0000000..8f3c661 --- /dev/null +++ b/www/assets/images/effection-logo.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/images/favicon-effection.png b/www/assets/images/favicon-effection.png new file mode 100644 index 0000000..e0c21d7 Binary files /dev/null and b/www/assets/images/favicon-effection.png differ diff --git a/www/assets/images/icon-effection.svg b/www/assets/images/icon-effection.svg new file mode 100644 index 0000000..7b189ee --- /dev/null +++ b/www/assets/images/icon-effection.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/www/assets/images/meta-effection.png b/www/assets/images/meta-effection.png new file mode 100644 index 0000000..6268ca9 Binary files /dev/null and b/www/assets/images/meta-effection.png differ diff --git a/www/assets/prism-atom-one-dark.css b/www/assets/prism-atom-one-dark.css new file mode 100644 index 0000000..f0daf7e --- /dev/null +++ b/www/assets/prism-atom-one-dark.css @@ -0,0 +1,492 @@ +/** + * Added to make line numbering and higlight selection work: + * https://github.com/timlrx/rehype-prism-plus#styling + */ +pre { + overflow-x: auto; +} + +/** + * Inspired by gatsby remark prism - https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs/ + * 1. Make the element just wide enough to fit its content. + * 2. Always fill the visible space in .code-highlight. + */ +.code-highlight { + float: left; /* 1 */ + min-width: 100%; /* 2 */ +} + +.code-line { + display: block; + padding-left: 16px; + padding-right: 16px; + margin-left: -16px; + margin-right: -16px; + border-left: 4px solid rgba(0, 0, 0, 0); /* Set placeholder for highlight accent border color to transparent */ +} + +.code-line.inserted { + background-color: rgba(16, 185, 129, 0.2); /* Set inserted line (+) color */ +} + +.code-line.deleted { + background-color: rgba(239, 68, 68, 0.2); /* Set deleted line (-) color */ +} + +.highlight-line { + margin-left: -16px; + margin-right: -16px; + background-color: rgba(55, 65, 81, 0.5); /* Set highlight bg color */ + border-left: 4px solid rgb(59, 130, 246); /* Set highlight accent border color */ +} + +.line-number::before { + display: inline-block; + width: 1rem; + text-align: right; + margin-right: 16px; + margin-left: -8px; + color: rgb(156, 163, 175); /* Line number color */ + content: attr(line); +} + +/** + * One Dark theme for prism.js + * Based on Atom's One Dark theme: https://github.com/atom/atom/tree/master/packages/one-dark-syntax + */ + +/** + * One Dark colours (accurate as of commit 8ae45ca on 6 Sep 2018) + * From colors.less + * --mono-1: hsl(220, 14%, 71%); + * --mono-2: hsl(220, 9%, 55%); + * --mono-3: hsl(220, 10%, 40%); + * --hue-1: hsl(187, 47%, 55%); + * --hue-2: hsl(207, 82%, 66%); + * --hue-3: hsl(286, 60%, 67%); + * --hue-4: hsl(95, 38%, 62%); + * --hue-5: hsl(355, 65%, 65%); + * --hue-5-2: hsl(5, 48%, 51%); + * --hue-6: hsl(29, 54%, 61%); + * --hue-6-2: hsl(39, 67%, 69%); + * --syntax-fg: hsl(220, 14%, 71%); + * --syntax-bg: hsl(220, 13%, 18%); + * --syntax-gutter: hsl(220, 14%, 45%); + * --syntax-guide: hsla(220, 14%, 71%, 0.15); + * --syntax-accent: hsl(220, 100%, 66%); + * From syntax-variables.less + * --syntax-selection-color: hsl(220, 13%, 28%); + * --syntax-gutter-background-color-selected: hsl(220, 13%, 26%); + * --syntax-cursor-line: hsla(220, 100%, 80%, 0.04); + */ + +code[class*="language-"], +pre[class*="language-"] { + background: hsl(220, 13%, 18%); + color: hsl(220, 14%, 71%); + text-shadow: 0 1px rgba(0, 0, 0, 0.3); + font-family: "Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Selection */ +code[class*="language-"]::-moz-selection, +code[class*="language-"] *::-moz-selection, +pre[class*="language-"] *::-moz-selection { + background: hsl(220, 13%, 28%); + color: inherit; + text-shadow: none; +} + +code[class*="language-"]::selection, +code[class*="language-"] *::selection, +pre[class*="language-"] *::selection { + background: hsl(220, 13%, 28%); + color: inherit; + text-shadow: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + border-radius: 0.3em; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: 0.2em 0.3em; + border-radius: 0.3em; + white-space: normal; +} + +/* Print */ +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +.token.comment, +.token.prolog, +.token.cdata { + color: hsl(220, 10%, 40%); +} + +.token.doctype, +.token.punctuation, +.token.entity { + color: hsl(220, 14%, 71%); +} + +.token.attr-name, +.token.class-name, +.token.boolean, +.token.constant, +.token.number, +.token.atrule { + color: hsl(29, 54%, 61%); +} + +.token.keyword { + color: hsl(286, 60%, 67%); +} + +.token.property, +.token.tag, +.token.symbol, +.token.deleted, +.token.important { + color: hsl(355, 65%, 65%); +} + +.token.selector, +.token.string, +.token.char, +.token.builtin, +.token.inserted, +.token.regex, +.token.attr-value, +.token.attr-value > .token.punctuation { + color: hsl(95, 38%, 62%); +} + +.token.variable, +.token.operator, +.token.function { + color: hsl(207, 82%, 66%); +} + +.token.url { + color: hsl(187, 47%, 55%); +} + +/* HTML overrides */ +.token.attr-value > .token.punctuation.attr-equals, +.token.special-attr > .token.attr-value > .token.value.css { + color: hsl(220, 14%, 71%); +} + +/* CSS overrides */ +.language-css .token.selector { + color: hsl(355, 65%, 65%); +} + +.language-css .token.property { + color: hsl(220, 14%, 71%); +} + +.language-css .token.function, +.language-css .token.url > .token.function { + color: hsl(187, 47%, 55%); +} + +.language-css .token.url > .token.string.url { + color: hsl(95, 38%, 62%); +} + +.language-css .token.important, +.language-css .token.atrule .token.rule { + color: hsl(286, 60%, 67%); +} + +/* JS overrides */ +.language-javascript .token.operator { + color: hsl(286, 60%, 67%); +} + +.language-javascript .token.template-string > .token.interpolation > .token.interpolation-punctuation.punctuation { + color: hsl(5, 48%, 51%); +} + +/* JSON overrides */ +.language-json .token.operator { + color: hsl(220, 14%, 71%); +} + +.language-json .token.null.keyword { + color: hsl(29, 54%, 61%); +} + +/* MD overrides */ +.language-markdown .token.url, +.language-markdown .token.url > .token.operator, +.language-markdown .token.url-reference.url > .token.string { + color: hsl(220, 14%, 71%); +} + +.language-markdown .token.url > .token.content { + color: hsl(207, 82%, 66%); +} + +.language-markdown .token.url > .token.url, +.language-markdown .token.url-reference.url { + color: hsl(187, 47%, 55%); +} + +.language-markdown .token.blockquote.punctuation, +.language-markdown .token.hr.punctuation { + color: hsl(220, 10%, 40%); + font-style: italic; +} + +.language-markdown .token.code-snippet { + color: hsl(95, 38%, 62%); +} + +.language-markdown .token.bold .token.content { + color: hsl(29, 54%, 61%); +} + +.language-markdown .token.italic .token.content { + color: hsl(286, 60%, 67%); +} + +.language-markdown .token.strike .token.content, +.language-markdown .token.strike .token.punctuation, +.language-markdown .token.list.punctuation, +.language-markdown .token.title.important > .token.punctuation { + color: hsl(355, 65%, 65%); +} + +/* General */ +.token.bold { + font-weight: bold; +} + +.token.comment, +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.namespace { + opacity: 0.8; +} + +/* Plugin overrides */ +/* Selectors should have higher specificity than those in the plugins' default stylesheets */ + +/* Show Invisibles plugin overrides */ +.token.token.tab:not(:empty):before, +.token.token.cr:before, +.token.token.lf:before, +.token.token.space:before { + color: hsla(220, 14%, 71%, 0.15); + text-shadow: none; +} + +/* Toolbar plugin overrides */ +/* Space out all buttons and move them away from the right edge of the code block */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item { + margin-right: 0.4em; +} + +/* Styling the buttons */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span { + background: hsl(220, 13%, 26%); + color: hsl(220, 9%, 55%); + padding: 0.1em 0.4em; + border-radius: 0.3em; +} + +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus { + background: hsl(220, 13%, 28%); + color: hsl(220, 14%, 71%); +} + +/* Line Highlight plugin overrides */ +/* The highlighted line itself */ +.line-highlight.line-highlight { + background: hsla(220, 100%, 80%, 0.04); +} + +/* Default line numbers in Line Highlight plugin */ +.line-highlight.line-highlight:before, +.line-highlight.line-highlight[data-end]:after { + background: hsl(220, 13%, 26%); + color: hsl(220, 14%, 71%); + padding: 0.1em 0.6em; + border-radius: 0.3em; + box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */ +} + +/* Hovering over a linkable line number (in the gutter area) */ +/* Requires Line Numbers plugin as well */ +pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before { + background-color: hsla(220, 100%, 80%, 0.04); +} + +/* Line Numbers and Command Line plugins overrides */ +/* Line separating gutter from coding area */ +.line-numbers.line-numbers .line-numbers-rows, +.command-line .command-line-prompt { + border-right-color: hsla(220, 14%, 71%, 0.15); +} + +/* Stuff in the gutter */ +.line-numbers .line-numbers-rows > span:before, +.command-line .command-line-prompt > span:before { + color: hsl(220, 14%, 45%); +} + +/* Match Braces plugin overrides */ +/* Note: Outline colour is inherited from the braces */ +.rainbow-braces .token.token.punctuation.brace-level-1, +.rainbow-braces .token.token.punctuation.brace-level-5, +.rainbow-braces .token.token.punctuation.brace-level-9 { + color: hsl(355, 65%, 65%); +} + +.rainbow-braces .token.token.punctuation.brace-level-2, +.rainbow-braces .token.token.punctuation.brace-level-6, +.rainbow-braces .token.token.punctuation.brace-level-10 { + color: hsl(95, 38%, 62%); +} + +.rainbow-braces .token.token.punctuation.brace-level-3, +.rainbow-braces .token.token.punctuation.brace-level-7, +.rainbow-braces .token.token.punctuation.brace-level-11 { + color: hsl(207, 82%, 66%); +} + +.rainbow-braces .token.token.punctuation.brace-level-4, +.rainbow-braces .token.token.punctuation.brace-level-8, +.rainbow-braces .token.token.punctuation.brace-level-12 { + color: hsl(286, 60%, 67%); +} + +/* Diff Highlight plugin overrides */ +/* Taken from https://github.com/atom/github/blob/master/styles/variables.less */ +pre.diff-highlight > code .token.token.deleted:not(.prefix), +pre > code.diff-highlight .token.token.deleted:not(.prefix) { + background-color: hsla(353, 100%, 66%, 0.15); +} + +pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection { + background-color: hsla(353, 95%, 66%, 0.25); +} + +pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection { + background-color: hsla(353, 95%, 66%, 0.25); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix), +pre > code.diff-highlight .token.token.inserted:not(.prefix) { + background-color: hsla(137, 100%, 55%, 0.15); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection { + background-color: hsla(135, 73%, 55%, 0.25); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection { + background-color: hsla(135, 73%, 55%, 0.25); +} + +/* Previewers plugin overrides */ +/* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-dark-ui */ +/* Border around popup */ +.prism-previewer.prism-previewer:before, +.prism-previewer-gradient.prism-previewer-gradient div { + border-color: hsl(224, 13%, 17%); +} + +/* Angle and time should remain as circles and are hence not included */ +.prism-previewer-color.prism-previewer-color:before, +.prism-previewer-gradient.prism-previewer-gradient div, +.prism-previewer-easing.prism-previewer-easing:before { + border-radius: 0.3em; +} + +/* Triangles pointing to the code */ +.prism-previewer.prism-previewer:after { + border-top-color: hsl(224, 13%, 17%); +} + +.prism-previewer-flipped.prism-previewer-flipped.after { + border-bottom-color: hsl(224, 13%, 17%); +} + +/* Background colour within the popup */ +.prism-previewer-angle.prism-previewer-angle:before, +.prism-previewer-time.prism-previewer-time:before, +.prism-previewer-easing.prism-previewer-easing { + background: hsl(219, 13%, 22%); +} + +/* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */ +/* For time, this is the alternate colour */ +.prism-previewer-angle.prism-previewer-angle circle, +.prism-previewer-time.prism-previewer-time circle { + stroke: hsl(220, 14%, 71%); + stroke-opacity: 1; +} + +/* Stroke colours of the handle, direction point, and vector itself */ +.prism-previewer-easing.prism-previewer-easing circle, +.prism-previewer-easing.prism-previewer-easing path, +.prism-previewer-easing.prism-previewer-easing line { + stroke: hsl(220, 14%, 71%); +} + +/* Fill colour of the handle */ +.prism-previewer-easing.prism-previewer-easing circle { + fill: transparent; +} diff --git a/www/components/navburger.tsx b/www/components/navburger.tsx new file mode 100644 index 0000000..1533386 --- /dev/null +++ b/www/components/navburger.tsx @@ -0,0 +1,13 @@ +//@ts-nocheck hastx does not currently typecheck correctly +export function Navburger() { + return ( + + Mobile menu + + + ); +} diff --git a/www/components/project-select.tsx b/www/components/project-select.tsx new file mode 100644 index 0000000..bfbd358 --- /dev/null +++ b/www/components/project-select.tsx @@ -0,0 +1,93 @@ +export function ProjectSelect() { + let uuid = self.crypto.randomUUID(); + + let toggleId = `toggle-${uuid}`; + let openerId = `opener-${uuid}`; + let closerId = `closer-${uuid}`; + + return ( + <> + + + OSS + + + +