Styles API docs (#1641)

Adds some basic API docs for the new styles API.

### Change Type

- [x] `documentation` — Changes to the documentation only[^2]

### Test Plan

--

### Release Notes
 --

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
alex 2023-06-24 15:01:02 +01:00 committed by GitHub
parent e8bc114bf3
commit aad5815a06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 281 additions and 59 deletions

View file

@ -1,12 +1,11 @@
import { DefaultColorStyle, ShapeProps, StyleProp } from '@tldraw/tldraw'
import { T } from '@tldraw/validate'
import { ICardShape, IWeightStyle } from './card-shape-types'
import { ICardShape } from './card-shape-types'
export const WeightStyle = new StyleProp<IWeightStyle>(
'myApp:weight',
'regular',
T.literalEnum('regular', 'bold')
)
export const WeightStyle = StyleProp.defineEnum('myApp:weight', {
defaultValue: 'regular',
values: ['regular', 'bold'],
})
// Validation for our custom card shape's props, using our custom style + one of tldraw's default styles
export const cardShapeProps: ShapeProps<ICardShape> = {

View file

@ -1786,7 +1786,7 @@ export function preventDefault(event: Event | React_2.BaseSyntheticEvent): void;
export { react }
// @public (undocumented)
// @public
export class ReadonlySharedStyleMap {
// (undocumented)
[Symbol.iterator](): IterableIterator<[StyleProp<unknown>, SharedStyle<unknown>]>;
@ -1801,7 +1801,7 @@ export class ReadonlySharedStyleMap {
getAsKnownValue<T>(prop: StyleProp<T>): T | undefined;
// (undocumented)
keys(): IterableIterator<StyleProp<unknown>>;
// (undocumented)
// @internal (undocumented)
protected map: Map<StyleProp<unknown>, SharedStyle<unknown>>;
// (undocumented)
get size(): number;
@ -1918,7 +1918,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
static type: string;
}
// @public (undocumented)
// @public
export type SharedStyle<T> = {
readonly type: 'mixed';
} | {

View file

@ -7769,9 +7769,16 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* A derived object containing either all current styles among the user's selected shapes, or
* else the user's most recent style choices that correspond to the current active state (i.e.
* the selected tool).
* A map of all the current styles either in the current selection, or that are relevant to the
* current tool.
*
* @example
* ```ts
* const color = editor.sharedStyles.get(DefaultColorStyle)
* if (color && color.type === 'shared') {
* console.log('All selected shapes have the same color:', color.value)
* }
* ```
*
* @public
*/
@ -7901,7 +7908,8 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* Set the current styles
* Set the value of a {@link @tldraw/tlschema#StyleProp}. This change will be applied to any
* selected shapes, and any subsequently created shapes.
*
* @example
* ```ts
@ -7911,8 +7919,10 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @param style - The style to set.
* @param value - The value to set.
* @param ephemeral - Whether the style change is ephemeral. Ephemeral changes don't get added to the undo/redo stack. Defaults to false.
* @param squashing - Whether the style change will be squashed into the existing history entry rather than creating a new one. Defaults to false.
* @param ephemeral - Whether the style change is ephemeral. Ephemeral changes don't get added
* to the undo/redo stack. Defaults to false.
* @param squashing - Whether the style change will be squashed into the existing history entry
* rather than creating a new one. Defaults to false.
*
* @public
*/

View file

@ -1,7 +1,16 @@
import { StyleProp } from '@tldraw/tlschema'
import { exhaustiveSwitchError } from '@tldraw/utils'
/** @public */
/**
* The value of a particular {@link @tldraw/tlschema#StyleProp}.
*
* A `mixed` style means that in the current selection, there are lots of different values for the
* same style prop - e.g. a red and a blue shape are selected.
*
* A `shared` style means that all shapes in the selection share the same value for this style prop.
*
* @public
*/
export type SharedStyle<T> =
| { readonly type: 'mixed' }
| { readonly type: 'shared'; readonly value: T }
@ -18,9 +27,16 @@ function sharedStyleEquals<T>(a: SharedStyle<T>, b: SharedStyle<T> | undefined)
}
}
/** @public */
/**
* A map of {@link @tldraw/tlschema#StyleProp | StyleProps} to their {@link SharedStyle} values. See
* {@link Editor.sharedStyles}.
*
* @public
*/
export class ReadonlySharedStyleMap {
/** @internal */
protected map: Map<StyleProp<unknown>, SharedStyle<unknown>>
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>) {
this.map = new Map(entries)
}

View file

@ -440,7 +440,7 @@ export const embedShapeProps: {
url: T.Validator<string>;
};
// @public (undocumented)
// @public
export class EnumStyleProp<T> extends StyleProp<T> {
// @internal
constructor(id: string, defaultValue: T, values: readonly T[]);
@ -723,18 +723,17 @@ export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
[K in keyof Shape['props']]: T.Validatable<Shape['props'][K]>;
};
// @public (undocumented)
// @public
export class StyleProp<Type> implements T.Validatable<Type> {
constructor(id: string, defaultValue: Type, type: T.Validatable<Type>);
// @internal
protected constructor(id: string, defaultValue: Type, type: T.Validatable<Type>);
// (undocumented)
readonly defaultValue: Type;
// (undocumented)
static define<Type>(uniqueId: string, { defaultValue, type }: {
static define<Type>(uniqueId: string, options: {
defaultValue: Type;
type?: T.Validatable<Type>;
}): StyleProp<Type>;
// (undocumented)
static defineEnum<const Values extends readonly unknown[]>(uniqueId: string, { defaultValue, values }: {
static defineEnum<const Values extends readonly unknown[]>(uniqueId: string, options: {
defaultValue: Values[number];
values: Values;
}): EnumStyleProp<Values[number]>;

View file

@ -1,22 +1,87 @@
import { T } from '@tldraw/validate'
/** @public */
/**
* A `StyleProp` is a property of a shape that follows some special rules.
*
* 1. The same value can be set on lots of shapes at the same time.
*
* 2. The last used value is automatically saved and applied to new shapes.
*
* For example, {@link DefaultColorStyle} is a style prop used by tldraw's default shapes to set
* their color. If you try selecting several shapes on tldraw.com and changing their color, you'll
* see that the color is applied to all of them. Then, if you draw a new shape, it'll have the same
* color as the one you just set.
*
* You can use styles in your own shapes by either defining your own (see {@link StyleProp.define}
* and {@link StyleProp.defineEnum}) or using tldraw's default ones, like {@link DefaultColorStyle}.
* When you define a shape, pass a `props` object describing all of your shape's properties, using
* `StyleProp`s for the ones you want to be styles. See the
* {@link https://github.com/tldraw/tldraw/tree/main/apps/examples/src/16-custom-styles | custom styles example}
* for more.
*
* @public
*/
export class StyleProp<Type> implements T.Validatable<Type> {
/**
* Define a new {@link StyleProp}.
*
* @param uniqueId - Each StyleProp must have a unique ID. We recommend you prefix this with
* your app/library name.
* @param options -
* - `defaultValue`: The default value for this style prop.
*
* - `type`: Optionally, describe what type of data you expect for this style prop.
*
* @example
* ```ts
* import {T} from '@tldraw/validate'
* import {StyleProp} from '@tldraw/tlschema'
*
* const MyLineWidthProp = StyleProp.define('myApp:lineWidth', {
* defaultValue: 1,
* type: T.number,
* })
* ```
* @public
*/
static define<Type>(
uniqueId: string,
{ defaultValue, type = T.any }: { defaultValue: Type; type?: T.Validatable<Type> }
options: { defaultValue: Type; type?: T.Validatable<Type> }
) {
const { defaultValue, type = T.any } = options
return new StyleProp<Type>(uniqueId, defaultValue, type)
}
/**
* Define a new {@link StyleProp} as a list of possible values.
*
* @param uniqueId - Each StyleProp must have a unique ID. We recommend you prefix this with
* your app/library name.
* @param options -
* - `defaultValue`: The default value for this style prop.
*
* - `values`: An array of possible values of this style prop.
*
* @example
* ```ts
* import {StyleProp} from '@tldraw/tlschema'
*
* const MySizeProp = StyleProp.defineEnum('myApp:size', {
* defaultValue: 'medium',
* values: ['small', 'medium', 'large'],
* })
* ```
*/
static defineEnum<const Values extends readonly unknown[]>(
uniqueId: string,
{ defaultValue, values }: { defaultValue: Values[number]; values: Values }
options: { defaultValue: Values[number]; values: Values }
) {
const { defaultValue, values } = options
return new EnumStyleProp<Values[number]>(uniqueId, defaultValue, values)
}
constructor(
/** @internal */
protected constructor(
readonly id: string,
readonly defaultValue: Type,
readonly type: T.Validatable<Type>
@ -27,7 +92,11 @@ export class StyleProp<Type> implements T.Validatable<Type> {
}
}
/** @public */
/**
* See {@link StyleProp} & {@link StyleProp.defineEnum}
*
* @public
*/
export class EnumStyleProp<T> extends StyleProp<T> {
/** @internal */
constructor(id: string, defaultValue: T, readonly values: readonly T[]) {

View file

@ -1,29 +1,67 @@
import { ApiItem } from '@microsoft/api-extractor-model'
import { ApiItem, ApiItemKind, ApiModel } from '@microsoft/api-extractor-model'
import {
DocCodeSpan,
DocEscapedText,
DocFencedCode,
DocLinkTag,
DocNode,
DocParagraph,
DocPlainText,
DocSection,
DocSoftBreak,
} from '@microsoft/tsdoc'
import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
import prettier from 'prettier'
export function getPath(item: { canonicalReference: ApiItem['canonicalReference'] }): string {
return item.canonicalReference
.toString()
.replace(/^@tldraw\/([^!]+)/, '$1/')
.replace(/[!:()#.]/g, '-')
function isOnParentPage(itemKind: ApiItemKind) {
switch (itemKind) {
case ApiItemKind.CallSignature:
case ApiItemKind.Class:
case ApiItemKind.EntryPoint:
case ApiItemKind.Enum:
case ApiItemKind.Function:
case ApiItemKind.Interface:
case ApiItemKind.Model:
case ApiItemKind.Namespace:
case ApiItemKind.Package:
case ApiItemKind.TypeAlias:
case ApiItemKind.Variable:
case ApiItemKind.None:
return false
case ApiItemKind.Constructor:
case ApiItemKind.ConstructSignature:
case ApiItemKind.EnumMember:
case ApiItemKind.Method:
case ApiItemKind.MethodSignature:
case ApiItemKind.Property:
case ApiItemKind.PropertySignature:
case ApiItemKind.IndexSignature:
return true
default:
exhaustiveSwitchError(itemKind)
}
}
function sanitizeReference(reference: string) {
return reference
.replace(/[!:()#.[\]]/g, '-')
.replace(/-+/g, '-')
.replace(/^-/, '')
.replace(/\/-/, '/')
.replace(/-$/, '')
}
export function getSlug(item: { canonicalReference: ApiItem['canonicalReference'] }): string {
return getPath(item).replace(/^[^/]+\//, '')
export function getSlug(item: ApiItem): string {
return sanitizeReference(item.canonicalReference.toString().replace(/^@tldraw\/[^!]+!/, ''))
}
export function getPath(item: ApiItem): string {
if (isOnParentPage(item.kind)) {
const parentPath = getPath(assertExists(item.parent))
const childSlug = getSlug(item)
return `${parentPath}#${childSlug}`
}
return sanitizeReference(item.canonicalReference.toString().replace(/^@tldraw\/([^!]+)/, '$1/'))
}
const prettierConfigPromise = prettier.resolveConfig(__dirname)
@ -57,12 +95,14 @@ export async function formatWithPrettier(
}
export class MarkdownWriter {
static async docNodeToMarkdown(docNode: DocNode) {
const writer = new MarkdownWriter()
static async docNodeToMarkdown(apiContext: ApiItem, docNode: DocNode) {
const writer = new MarkdownWriter(apiContext)
await writer.writeDocNode(docNode)
return writer.toString()
}
private constructor(private readonly apiContext: ApiItem) {}
private result = ''
write(...parts: string[]): this {
@ -102,6 +142,37 @@ export class MarkdownWriter {
)
} else if (docNode instanceof DocEscapedText) {
this.write(docNode.encodedText)
} else if (docNode instanceof DocLinkTag) {
if (docNode.urlDestination) {
this.write(
'[',
docNode.linkText ?? docNode.urlDestination,
'](',
docNode.urlDestination,
')'
)
} else {
assert(docNode.codeDestination)
const apiModel = getTopLevelModel(this.apiContext)
const refResult = apiModel.resolveDeclarationReference(
docNode.codeDestination,
this.apiContext
)
if (refResult.errorMessage) {
throw new Error(refResult.errorMessage)
}
const linkedItem = assertExists(refResult.resolvedApiItem)
const path = getPath(linkedItem)
this.write(
'[',
docNode.linkText ?? getDefaultReferenceText(linkedItem),
'](/gen/',
path,
')'
)
}
} else {
throw new Error(`Unknown docNode kind: ${docNode.kind}`)
}
@ -118,3 +189,49 @@ export class MarkdownWriter {
return this.result
}
}
function getDefaultReferenceText(item: ApiItem): string {
function parentPrefix(str: string, sep = '.'): string {
if (!item.parent) return str
return `${getDefaultReferenceText(item.parent)}${sep}${str}`
}
switch (item.kind) {
case ApiItemKind.CallSignature:
return parentPrefix(`${item.displayName}()`)
case ApiItemKind.Constructor:
case ApiItemKind.ConstructSignature: {
const parent = assertExists(item.parent)
return `new ${getDefaultReferenceText(parent)}()`
}
case ApiItemKind.EnumMember:
case ApiItemKind.Method:
case ApiItemKind.MethodSignature:
case ApiItemKind.Property:
case ApiItemKind.PropertySignature:
return parentPrefix(item.displayName)
case ApiItemKind.IndexSignature:
return parentPrefix(`[${item.displayName}]`, '')
case ApiItemKind.Class:
case ApiItemKind.EntryPoint:
case ApiItemKind.Enum:
case ApiItemKind.Function:
case ApiItemKind.Interface:
case ApiItemKind.Model:
case ApiItemKind.Namespace:
case ApiItemKind.Package:
case ApiItemKind.TypeAlias:
case ApiItemKind.Variable:
case ApiItemKind.None:
return item.displayName
default:
exhaustiveSwitchError(item.kind)
}
}
function getTopLevelModel(item: ApiItem): ApiModel {
const model = assertExists(item.getAssociatedModel())
if (model.parent) {
return getTopLevelModel(model.parent)
}
return model
}

View file

@ -28,25 +28,22 @@ async function generateApiDocs() {
fs.mkdirSync(OUTPUT_DIR)
// to include more packages in docs, add them to devDependencies in package.json
const tldrawPackagesToIncludeInDocs = [
'@tldraw/editor',
'@tldraw/file-format',
'@tldraw/primitives',
'@tldraw/store',
'@tldraw/tldraw',
'@tldraw/tlschema',
'@tldraw/ui',
'@tldraw/validate',
]
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'))
const tldrawPackagesToIncludeInDocs = Object.keys(packageJson.devDependencies).filter((dep) =>
dep.startsWith('@tldraw/')
)
const model = new ApiModel()
const packageModels = []
for (const packageName of tldrawPackagesToIncludeInDocs) {
// Get the file contents
const filePath = path.join(INPUT_DIR, packageName.replace('@tldraw/', ''), 'api', 'api.json')
try {
const packageModel = model.loadPackage(filePath)
packageModels.push(model.loadPackage(filePath))
}
for (const packageModel of packageModels) {
try {
const categoryName = packageModel.name.replace(`@tldraw/`, '')
if (!addedCategories.has(categoryName)) {
@ -97,7 +94,7 @@ async function generateApiDocs() {
fs.writeFileSync(path.join(OUTPUT_DIR, outputFileName), result.markdown)
}
} catch (e: any) {
throw Error(`Could not create API docs for ${packageName}: ${e.message}`)
throw Error(`Could not create API docs for ${packageModel.name}: ${e.message}`)
}
}

View file

@ -22,7 +22,7 @@ import {
Excerpt,
ReleaseTag,
} from '@microsoft/api-extractor-model'
import assert from 'assert'
import { assert, assertExists } from '@tldraw/utils'
import { MarkdownWriter, formatWithPrettier, getPath, getSlug } from './docs-utils'
type Result = { markdown: string }
@ -171,7 +171,10 @@ async function addDocComment(result: Result, member: ApiItem) {
}
if (member.tsdocComment) {
result.markdown += await MarkdownWriter.docNodeToMarkdown(member.tsdocComment.summarySection)
result.markdown += await MarkdownWriter.docNodeToMarkdown(
member,
member.tsdocComment.summarySection
)
const exampleBlocks = member.tsdocComment.customBlocks.filter(
(block) => block.blockTag.tagNameWithUpperCase === '@EXAMPLE'
@ -181,7 +184,7 @@ async function addDocComment(result: Result, member: ApiItem) {
result.markdown += `\n\n`
result.markdown += `##### Example\n\n`
for (const example of exampleBlocks) {
result.markdown += await MarkdownWriter.docNodeToMarkdown(example.content)
result.markdown += await MarkdownWriter.docNodeToMarkdown(member, example.content)
}
}
}
@ -214,7 +217,10 @@ async function addDocComment(result: Result, member: ApiItem) {
})
result.markdown += `\n\n`
if (param.tsdocParamBlock) {
result.markdown += await MarkdownWriter.docNodeToMarkdown(param.tsdocParamBlock.content)
result.markdown += await MarkdownWriter.docNodeToMarkdown(
member,
param.tsdocParamBlock.content
)
}
result.markdown += `\n\n</ParametersTableDescription>\n`
result.markdown += `</ParametersTableRow>\n`
@ -230,6 +236,7 @@ async function addDocComment(result: Result, member: ApiItem) {
result.markdown += `\n\n`
if (member.tsdocComment && member.tsdocComment.returnsBlock) {
result.markdown += await MarkdownWriter.docNodeToMarkdown(
member,
member.tsdocComment.returnsBlock.content
)
}
@ -254,7 +261,7 @@ async function addDocComment(result: Result, member: ApiItem) {
result.markdown += `\`${block.parameterName}\`\n\n`
result.markdown += `</ParametersTableName>\n`
result.markdown += `<ParametersTableDescription>\n\n`
result.markdown += await MarkdownWriter.docNodeToMarkdown(block.content)
result.markdown += await MarkdownWriter.docNodeToMarkdown(member, block.content)
result.markdown += `\n\n</ParametersTableDescription>\n`
result.markdown += `</ParametersTableRow>\n`
}
@ -347,7 +354,15 @@ function addReferences(result: Result, member: ApiItem) {
member.excerptTokens.forEach((token) => {
if (token.kind !== 'Reference') return
const url = `/gen/${getPath(token as { canonicalReference: ApiItem['canonicalReference'] })}`
const apiItemResult = assertExists(member.getAssociatedModel()).resolveDeclarationReference(
assertExists(token.canonicalReference),
member
)
if (apiItemResult.errorMessage) {
return
}
const apiItem = assertExists(apiItemResult.resolvedApiItem)
const url = `/gen/${getPath(apiItem)}`
references.add(`[${token.text}](${url})`)
})