mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
Merge branch 'cal/fix-moderation-pkg' of github.com:modrinth/code into chyz/checklist-dump
# Conflicts: # apps/frontend/src/pages/[type]/[project].vue # standards/frontend/COMPONENT_STRUCTURE.md
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { Component, FunctionalComponent, SVGAttributes } from 'vue'
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import { Priority } from '../priority.ts'
|
||||
@@ -255,39 +255,35 @@ export function withOnClick<T extends object>(node: T): T & Clickable {
|
||||
})
|
||||
}
|
||||
|
||||
export interface ComponentNodePropsContext {
|
||||
export interface NodePropsContext {
|
||||
onImageUpload?: (file: File) => Promise<string>
|
||||
toggleSetValue?: (value: string) => void
|
||||
nodeFacts?: { needsAttention: boolean; fixActionable: boolean }
|
||||
tooltip?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type BuiltinRendererKey = 'action' | 'checkbox' | 'toggle' | 'dropdown' | 'text' | 'markdown'
|
||||
|
||||
export type NodeRendererDescriptor = { type: BuiltinRendererKey } | { type: 'custom'; key: string }
|
||||
|
||||
export interface Renderable {
|
||||
_component: Component | undefined
|
||||
_rendererKey: string | undefined
|
||||
_componentProps?: (ctx: ComponentNodePropsContext) => Record<string, unknown>
|
||||
_renderer: NodeRendererDescriptor
|
||||
_modelProp: string
|
||||
}
|
||||
|
||||
export function withComponent<T extends object>(
|
||||
export function withRenderer<T extends object>(
|
||||
node: T,
|
||||
opts: {
|
||||
component?: Component
|
||||
rendererKey?: string
|
||||
renderer: NodeRendererDescriptor
|
||||
modelProp?: string
|
||||
componentProps?: Renderable['_componentProps']
|
||||
},
|
||||
): T & Renderable {
|
||||
return Object.assign(node, {
|
||||
_component: opts.component,
|
||||
_rendererKey: opts.rendererKey,
|
||||
_componentProps: opts.componentProps,
|
||||
_renderer: opts.renderer,
|
||||
_modelProp: opts.modelProp ?? 'modelValue',
|
||||
})
|
||||
}
|
||||
|
||||
export interface Configurable {
|
||||
_extraProps: ((ctx: ComponentNodePropsContext) => Record<string, unknown>) | undefined
|
||||
_extraProps: ((ctx: NodePropsContext) => Record<string, unknown>) | undefined
|
||||
}
|
||||
|
||||
export function withExtraProps<T extends object>(node: T): T & Configurable {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip="tooltip"
|
||||
:type="color === 'standard' ? 'base' : 'colored'"
|
||||
:color="color === 'standard' ? undefined : color"
|
||||
:disabled="disabled"
|
||||
:aria-label="icon ? label : undefined"
|
||||
@click="emit('update:modelValue', !modelValue)"
|
||||
>
|
||||
<component :is="icon" v-if="icon" />
|
||||
<template v-else>{{ label }}</template>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Button } from '@modrinth/ui'
|
||||
import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
label?: string
|
||||
icon?: Component
|
||||
disabled?: boolean
|
||||
needsAttention?: boolean
|
||||
fixActionable?: boolean
|
||||
tooltip?: Record<string, unknown>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [boolean]
|
||||
}>()
|
||||
|
||||
const color = computed(() => {
|
||||
if (!props.modelValue) return 'standard'
|
||||
if (props.needsAttention) return 'orange'
|
||||
return props.fixActionable ? 'blue' : 'brand'
|
||||
})
|
||||
</script>
|
||||
@@ -1,401 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { Button, IconButton } from '@modrinth/ui'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import type { Component } from 'vue'
|
||||
import { computed, inject, watchEffect } from 'vue'
|
||||
|
||||
import type { AnyNode, ChildNode, HasChildren } from '../builder'
|
||||
import type {
|
||||
ComponentNodePropsContext,
|
||||
Enableable,
|
||||
HasValue,
|
||||
Identified,
|
||||
OnChangeFn,
|
||||
TweakDef,
|
||||
} from '../capabilities'
|
||||
import { CHECKLIST_META_KEY } from '../context'
|
||||
import type { Writer } from '../mutate'
|
||||
import { childWriter, originScope, writeNodeValue } from '../mutate'
|
||||
import {
|
||||
getBooleanChildState,
|
||||
getEffectiveValue,
|
||||
hasCap,
|
||||
hasChildrenCap,
|
||||
hasIdCap,
|
||||
hasOptionsCap,
|
||||
hasValueCap,
|
||||
isNodeActive,
|
||||
isShown,
|
||||
resolveChildren,
|
||||
withStateDefaults,
|
||||
} from '../resolve'
|
||||
import type { NodeState, Reactive } from '../state'
|
||||
import { resolve } from '../state'
|
||||
import ActionButton from './ActionButton.vue'
|
||||
|
||||
const metaCtx = inject(CHECKLIST_META_KEY)
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: ChildNode[]
|
||||
state: Record<string, NodeState>
|
||||
write: Writer
|
||||
onImageUpload?: (file: File) => Promise<string>
|
||||
flex?: boolean
|
||||
titleDepth?: number
|
||||
appComponents?: Record<string, Component>
|
||||
globalState?: Record<string, Record<string, NodeState>>
|
||||
}>()
|
||||
|
||||
type RenderableValueNode = AnyNode &
|
||||
HasValue &
|
||||
Identified &
|
||||
Partial<Enableable> & {
|
||||
_component: Component | undefined
|
||||
_rendererKey: string | undefined
|
||||
_modelProp: string
|
||||
_componentProps?: (ctx: ComponentNodePropsContext) => Record<string, unknown>
|
||||
_extraProps?: (ctx: ComponentNodePropsContext) => Record<string, unknown>
|
||||
_tweaks?: TweakDef[]
|
||||
}
|
||||
|
||||
function resolveComponent(node: RenderableValueNode): Component | undefined {
|
||||
return (
|
||||
node._component ?? (node._rendererKey ? props.appComponents?.[node._rendererKey] : undefined)
|
||||
)
|
||||
}
|
||||
|
||||
function titleClass(depth: number): string {
|
||||
if (depth === 0) return 'text-lg font-extrabold text-contrast'
|
||||
if (depth === 1) return 'text-base font-semibold'
|
||||
if (depth === 2) return 'text-sm font-semibold'
|
||||
return ''
|
||||
}
|
||||
|
||||
function getTitle(node: object): string | undefined {
|
||||
if (!hasCap(node, '_title')) return undefined
|
||||
const title = node._title as Reactive<string> | undefined
|
||||
if (title === undefined) return undefined
|
||||
return resolve(title) || undefined
|
||||
}
|
||||
|
||||
function needsAttention(node: object): boolean {
|
||||
return metaCtx?.value.attentionMap.get(node) ?? false
|
||||
}
|
||||
|
||||
function isFixActionable(node: object): boolean {
|
||||
return metaCtx?.value.metaMap.get(node)?.isFixActionable ?? false
|
||||
}
|
||||
|
||||
const wrappedState = computed(() => withStateDefaults(props.state, props.nodes, props.write))
|
||||
|
||||
function isEnabled(node: Partial<Enableable>): boolean {
|
||||
if (node._enabled === undefined) return true
|
||||
if (typeof node._enabled === 'function') return node._enabled(wrappedState.value)
|
||||
return resolve(node._enabled)
|
||||
}
|
||||
|
||||
function toggleSetValue(node: RenderableValueNode, value: string): void {
|
||||
const current = getEffectiveValue(
|
||||
node,
|
||||
props.state[node.id],
|
||||
wrappedState.value,
|
||||
) as unknown as string[]
|
||||
const set = new Set(Array.isArray(current) ? current : [])
|
||||
if (set.has(value)) set.delete(value)
|
||||
else set.add(value)
|
||||
writeNodeValue(node, props.state, props.write, Array.from(set) as never, wrappedState.value)
|
||||
}
|
||||
|
||||
const DROPDOWN_TRIGGER_CHROME_PX = 16 * 2 + 10 + 20 + 2
|
||||
let measureEl: HTMLSpanElement | null = null
|
||||
function measureLabelWidth(label: string): number {
|
||||
if (typeof document === 'undefined') return 0
|
||||
if (!measureEl) {
|
||||
measureEl = document.createElement('span')
|
||||
measureEl.className = 'min-w-0 truncate text-primary font-semibold leading-tight'
|
||||
Object.assign(measureEl.style, {
|
||||
position: 'absolute',
|
||||
visibility: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
left: '-9999px',
|
||||
top: '0',
|
||||
})
|
||||
document.body.appendChild(measureEl)
|
||||
}
|
||||
measureEl.textContent = label
|
||||
return measureEl.getBoundingClientRect().width
|
||||
}
|
||||
|
||||
const dropdownMinWidthCache = new Map<string, string>()
|
||||
function getDropdownMinWidth(options: { label: string }[]): string {
|
||||
const key = options.map((o) => o.label).join(' ')
|
||||
const cached = dropdownMinWidthCache.get(key)
|
||||
if (cached) return cached
|
||||
const maxLabelWidth = Math.max(0, ...options.map((o) => measureLabelWidth(o.label)))
|
||||
const result = `${Math.ceil(maxLabelWidth) + DROPDOWN_TRIGGER_CHROME_PX}px`
|
||||
dropdownMinWidthCache.set(key, result)
|
||||
return result
|
||||
}
|
||||
|
||||
function componentProps(node: RenderableValueNode): Record<string, unknown> {
|
||||
const ctx: ComponentNodePropsContext = {
|
||||
onImageUpload: props.onImageUpload,
|
||||
toggleSetValue: (value) => toggleSetValue(node, value),
|
||||
nodeFacts: { needsAttention: needsAttention(node), fixActionable: isFixActionable(node) },
|
||||
tooltip: resolveTooltip(node),
|
||||
}
|
||||
const dropdownStyle = hasOptionsCap(node)
|
||||
? {
|
||||
class: '!w-auto max-w-full',
|
||||
style: { minWidth: getDropdownMinWidth(node._options as unknown as { label: string }[]) },
|
||||
}
|
||||
: undefined
|
||||
return {
|
||||
disabled: !isEnabled(node),
|
||||
...dropdownStyle,
|
||||
...node._componentProps?.(ctx),
|
||||
...node._extraProps?.(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
function containerScope(node: HasChildren & Partial<Identified>): {
|
||||
state: Record<string, NodeState>
|
||||
write: Writer
|
||||
} {
|
||||
if (hasCap(node, '_stateOrigin') && node._stateOrigin && props.globalState) {
|
||||
return originScope(props.globalState, node._stateOrigin as string[])
|
||||
}
|
||||
if (!hasIdCap(node)) return { state: props.state, write: props.write }
|
||||
const raw = props.state[node.id]
|
||||
const state =
|
||||
raw && typeof raw === 'object' && !(raw instanceof Set)
|
||||
? (raw as Record<string, NodeState>)
|
||||
: {}
|
||||
return { state, write: childWriter(props.state, props.write, node.id) }
|
||||
}
|
||||
|
||||
function valueScope(node: HasValue & Identified): {
|
||||
state: Record<string, NodeState>
|
||||
write: Writer
|
||||
} {
|
||||
const state = getBooleanChildState(props.state[node.id])
|
||||
return { state, write: childWriter(props.state, props.write, node.id) }
|
||||
}
|
||||
|
||||
const TOOLTIP_BASE = {
|
||||
delay: { show: 500, hide: 0 },
|
||||
triggers: ['hover', 'focus'],
|
||||
placement: 'top',
|
||||
}
|
||||
|
||||
function resolveTooltip(node: object): Record<string, unknown> | undefined {
|
||||
if (hasCap(node, '_tooltip')) {
|
||||
const t = node._tooltip as
|
||||
| Reactive<string>
|
||||
| ((state: Record<string, NodeState>) => string)
|
||||
| undefined
|
||||
if (t !== undefined) {
|
||||
const content = typeof t === 'function' ? t(wrappedState.value) : resolve(t)
|
||||
if (content) return { ...TOOLTIP_BASE, content }
|
||||
}
|
||||
}
|
||||
const hasSegments = hasCap(node, '_segments')
|
||||
const html = hasSegments ? metaCtx?.value.tooltipHtml.get(node) : undefined
|
||||
return html ? { ...TOOLTIP_BASE, content: html, html: true } : undefined
|
||||
}
|
||||
|
||||
function clickButton(node: object): void {
|
||||
if (!hasCap(node, '_onClick')) return
|
||||
;(node._onClick as (state: Record<string, NodeState>) => void)?.(wrappedState.value)
|
||||
}
|
||||
|
||||
function tweakCurrent(node: RenderableValueNode): unknown {
|
||||
return getEffectiveValue(node, props.state[node.id], wrappedState.value)
|
||||
}
|
||||
|
||||
function tweakResult(tweak: TweakDef, node: RenderableValueNode): unknown {
|
||||
return tweak.compute(tweakCurrent(node), wrappedState.value)
|
||||
}
|
||||
|
||||
function tweakEnabled(tweak: TweakDef, node: RenderableValueNode): boolean {
|
||||
const result = tweakResult(tweak, node)
|
||||
return result !== null && result !== undefined && result !== tweakCurrent(node)
|
||||
}
|
||||
|
||||
function tweakTooltip(
|
||||
tweak: TweakDef,
|
||||
node: RenderableValueNode,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!tweakEnabled(tweak, node)) return undefined
|
||||
const content = tweakResult(tweak, node)
|
||||
return content ? { ...TOOLTIP_BASE, content: String(content) } : undefined
|
||||
}
|
||||
|
||||
function tweakLabel(tweak: TweakDef, node: RenderableValueNode): string {
|
||||
const result = tweakResult(tweak, node)
|
||||
return result !== null && result !== undefined ? String(result) : 'Apply suggested value'
|
||||
}
|
||||
|
||||
function applyTweak(tweak: TweakDef, node: RenderableValueNode): void {
|
||||
const result = tweakResult(tweak, node)
|
||||
if (result !== null && result !== undefined) {
|
||||
updateValue(node, result)
|
||||
}
|
||||
}
|
||||
|
||||
function nodeKey(item: ChildNode, idx: number): string {
|
||||
return typeof item === 'object' && item !== null && hasIdCap(item) ? item.id : `n-${idx}`
|
||||
}
|
||||
|
||||
function modelProp(item: object): string {
|
||||
return (item as RenderableValueNode)._modelProp
|
||||
}
|
||||
|
||||
function updateEvent(item: object): string {
|
||||
return `update:${modelProp(item)}`
|
||||
}
|
||||
|
||||
function updateValue(item: RenderableValueNode, v: unknown): void {
|
||||
const onChange = hasCap(item, '_onChange')
|
||||
? (item._onChange as OnChangeFn | undefined)
|
||||
: undefined
|
||||
if (onChange) {
|
||||
const result = onChange(v as string, { override: (ov) => ({ __override: ov }) })
|
||||
if (result && typeof result === 'object' && '__override' in result) {
|
||||
writeNodeValue(item, props.state, props.write, result.__override as never, wrappedState.value)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeNodeValue(item, props.state, props.write, v as never, wrappedState.value)
|
||||
}
|
||||
|
||||
const seenOnChangeValues = new Map<object, unknown>()
|
||||
|
||||
watchEffect(() => {
|
||||
for (const node of props.nodes) {
|
||||
if (typeof node !== 'object' || node === null) continue
|
||||
if (!hasCap(node, '_onChange') || !(node as { _onChange: unknown })._onChange) continue
|
||||
if (!hasValueCap(node) || !hasIdCap(node)) continue
|
||||
if (!isShown(node as AnyNode)) continue
|
||||
const value = getEffectiveValue(node, props.state[node.id], wrappedState.value)
|
||||
if (seenOnChangeValues.has(node) && seenOnChangeValues.get(node) === value) continue
|
||||
seenOnChangeValues.set(node, value)
|
||||
const onChange = (node as RenderableValueNode)._onChange as OnChangeFn | undefined
|
||||
onChange?.(value as never, { override: (ov) => ({ __override: ov }) })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[flex ? 'flex flex-wrap gap-2' : 'space-y-4', 'w-full']">
|
||||
<template v-for="(item, idx) in nodes" :key="nodeKey(item, idx)">
|
||||
<template v-if="typeof item !== 'object' || item === null">
|
||||
<template v-if="typeof item === 'string'">{{ item }}</template>
|
||||
<component :is="item" v-else />
|
||||
</template>
|
||||
|
||||
<template v-else-if="isShown(item)">
|
||||
<div
|
||||
:class="
|
||||
hasChildrenCap(item) && !hasValueCap(item)
|
||||
? 'w-full'
|
||||
: !getTitle(item)
|
||||
? 'contents'
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<div v-if="getTitle(item)" class="mb-2" :class="titleClass(titleDepth ?? 0)">
|
||||
<!-- eslint-disable vue/no-v-html -- title text is author-controlled (stage definitions), not user input -->
|
||||
<span
|
||||
v-html="renderString(getTitle(item)!).replace(/^<p>([\s\S]*)<\/p>\n?$/, '$1')"
|
||||
/><span v-if="needsAttention(item)" class="text-red">*</span>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
|
||||
<template v-if="hasChildrenCap(item) && !hasValueCap(item)">
|
||||
<NodeRenderer
|
||||
:nodes="resolveChildren(item, containerScope(item).state)"
|
||||
:state="containerScope(item).state"
|
||||
:write="containerScope(item).write"
|
||||
:on-image-upload="onImageUpload"
|
||||
:app-components="appComponents"
|
||||
:global-state="globalState"
|
||||
:flex="(item as any)._layout !== 'column'"
|
||||
:title-depth="getTitle(item) !== undefined ? (titleDepth ?? 0) + 1 : titleDepth"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="hasValueCap(item) && hasIdCap(item)">
|
||||
<component
|
||||
:is="resolveComponent(item as RenderableValueNode)"
|
||||
v-if="resolveComponent(item as RenderableValueNode) === ActionButton"
|
||||
v-bind="componentProps(item as RenderableValueNode)"
|
||||
:[modelProp(item)]="
|
||||
getEffectiveValue(item as RenderableValueNode, state[item.id], wrappedState)
|
||||
"
|
||||
@[updateEvent(item)]="(v: unknown) => updateValue(item as RenderableValueNode, v)"
|
||||
/>
|
||||
<component
|
||||
:is="resolveComponent(item as RenderableValueNode)"
|
||||
v-else
|
||||
v-tooltip="resolveTooltip(item)"
|
||||
v-bind="componentProps(item as RenderableValueNode)"
|
||||
:[modelProp(item)]="
|
||||
getEffectiveValue(item as RenderableValueNode, state[item.id], wrappedState)
|
||||
"
|
||||
@[updateEvent(item)]="(v: unknown) => updateValue(item as RenderableValueNode, v)"
|
||||
/>
|
||||
<template
|
||||
v-for="(tweak, tIdx) in (item as RenderableValueNode)._tweaks ?? []"
|
||||
:key="`tweak-${tIdx}`"
|
||||
>
|
||||
<IconButton
|
||||
v-tooltip="tweakTooltip(tweak, item as RenderableValueNode)"
|
||||
:label="tweakLabel(tweak, item as RenderableValueNode)"
|
||||
:disabled="!tweakEnabled(tweak, item as RenderableValueNode)"
|
||||
@click="applyTweak(tweak, item as RenderableValueNode)"
|
||||
>
|
||||
<component :is="tweak.icon" />
|
||||
</IconButton>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="hasCap(item, '_onClick')">
|
||||
<Button
|
||||
v-tooltip="resolveTooltip(item)"
|
||||
:disabled="!isEnabled(item as any)"
|
||||
:aria-label="(item as any)._icon ? (item as any).label : undefined"
|
||||
@click="clickButton(item)"
|
||||
>
|
||||
<component :is="(item as any)._icon" v-if="(item as any)._icon" />
|
||||
<template v-else>{{ (item as any).label }}</template>
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-for="(item, idx) in nodes" :key="`children-${nodeKey(item, idx)}`">
|
||||
<NodeRenderer
|
||||
v-if="
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
isShown(item) &&
|
||||
hasValueCap(item) &&
|
||||
hasIdCap(item) &&
|
||||
hasChildrenCap(item) &&
|
||||
isNodeActive(item, state[item.id], wrappedState) &&
|
||||
resolveChildren(item, valueScope(item).state).length
|
||||
"
|
||||
:nodes="resolveChildren(item, valueScope(item).state)"
|
||||
:state="valueScope(item).state"
|
||||
:write="valueScope(item).write"
|
||||
:on-image-upload="onImageUpload"
|
||||
:app-components="appComponents"
|
||||
:global-state="globalState"
|
||||
:title-depth="getTitle(item) !== undefined ? (titleDepth ?? 0) + 1 : titleDepth"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
import type { NodeMeta } from './node-meta'
|
||||
|
||||
export interface ChecklistMetaContext {
|
||||
metaMap: Map<object, NodeMeta>
|
||||
attentionMap: Map<object, boolean>
|
||||
tooltipHtml: Map<object, string>
|
||||
}
|
||||
|
||||
export const CHECKLIST_META_KEY: InjectionKey<Ref<ChecklistMetaContext>> = Symbol('checklistMeta')
|
||||
@@ -1,10 +1,6 @@
|
||||
import { Checkbox, Combobox, MarkdownEditor, StyledInput, Toggle } from '@modrinth/ui'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import { withAutoProps, withChildren } from './builder'
|
||||
import type { ComponentNodePropsContext, Configurable } from './capabilities'
|
||||
import type { Configurable, NodePropsContext } from './capabilities'
|
||||
import {
|
||||
withComponent,
|
||||
withEditable,
|
||||
withEnabled,
|
||||
withExtraProps,
|
||||
@@ -16,6 +12,7 @@ import {
|
||||
withNoneLabel,
|
||||
withOnClick,
|
||||
withPriority,
|
||||
withRenderer,
|
||||
withRequired,
|
||||
withSelectable,
|
||||
withShown,
|
||||
@@ -26,7 +23,6 @@ import {
|
||||
withTweak,
|
||||
withValue,
|
||||
} from './capabilities'
|
||||
import ActionButton from './components/ActionButton.vue'
|
||||
import { pipe } from './pipe'
|
||||
import type { NodeState, NodeStateWithChildren } from './state'
|
||||
|
||||
@@ -68,17 +64,7 @@ export function toggle(id: string, label: string) {
|
||||
withFix,
|
||||
withEnabled,
|
||||
(n) => withValue(n, booleanValue),
|
||||
(n) =>
|
||||
withComponent(n, {
|
||||
component: markRaw(ActionButton),
|
||||
componentProps: (ctx) => ({
|
||||
label: n.label,
|
||||
icon: n._icon,
|
||||
needsAttention: ctx.nodeFacts?.needsAttention ?? false,
|
||||
fixActionable: ctx.nodeFacts?.fixActionable ?? false,
|
||||
tooltip: ctx.tooltip,
|
||||
}),
|
||||
}),
|
||||
(n) => withRenderer(n, { renderer: { type: 'action' } }),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -108,11 +94,7 @@ export function check(id: string, label: string) {
|
||||
withFix,
|
||||
withEnabled,
|
||||
(n) => withValue(n, booleanValue),
|
||||
(n) =>
|
||||
withComponent(n, {
|
||||
component: markRaw(Checkbox),
|
||||
componentProps: () => ({ label }),
|
||||
}),
|
||||
(n) => withRenderer(n, { renderer: { type: 'checkbox' } }),
|
||||
withExtraProps,
|
||||
),
|
||||
)
|
||||
@@ -133,7 +115,7 @@ export function toggleSwitch(id: string, label: string) {
|
||||
withFix,
|
||||
withEnabled,
|
||||
(n) => withValue(n, booleanValue),
|
||||
(n) => withComponent(n, { component: markRaw(Toggle) }),
|
||||
(n) => withRenderer(n, { renderer: { type: 'toggle' } }),
|
||||
withExtraProps,
|
||||
),
|
||||
)
|
||||
@@ -227,19 +209,7 @@ export function dropdown(id: string) {
|
||||
(n) => withValue(n, dropdownValue),
|
||||
withNoneLabel,
|
||||
withOptions,
|
||||
(n) =>
|
||||
withComponent(n, {
|
||||
component: markRaw(Combobox),
|
||||
componentProps: () => ({
|
||||
options: [
|
||||
...(n._none !== undefined ? [{ value: '', label: n._none }] : []),
|
||||
...n._options.map((o) => ({ value: o.value, label: o.label })),
|
||||
],
|
||||
triggerClass:
|
||||
'!bg-[var(--color-button-bg)] !rounded-[var(--radius-md)] !shadow-[var(--shadow-inset-sm),0_0_0_0_transparent]',
|
||||
dropdownClass: '!rounded-[var(--radius-md)] !bg-[var(--color-button-bg)] !border-0',
|
||||
}),
|
||||
}),
|
||||
(n) => withRenderer(n, { renderer: { type: 'dropdown' } }),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -278,11 +248,7 @@ export function text(id: string) {
|
||||
(n) => Object.assign(n, { _showTooltip: true, _imperativeSync: true }),
|
||||
(n) => withValue(n, stringValue),
|
||||
withTweak,
|
||||
(n) =>
|
||||
withComponent(n, {
|
||||
component: markRaw(StyledInput),
|
||||
componentProps: () => ({ class: 'min-w-40 flex-1', autocomplete: 'off' }),
|
||||
}),
|
||||
(n) => withRenderer(n, { renderer: { type: 'text' } }),
|
||||
withExtraProps,
|
||||
),
|
||||
)
|
||||
@@ -304,16 +270,7 @@ export function markdown(id: string) {
|
||||
withEnabled,
|
||||
withEditable,
|
||||
(n) => withValue(n, stringValue),
|
||||
(n) =>
|
||||
withComponent(n, {
|
||||
component: markRaw(MarkdownEditor),
|
||||
componentProps: (ctx) => ({
|
||||
maxHeight: 300,
|
||||
disabled: false,
|
||||
headingButtons: false,
|
||||
onImageUpload: ctx.onImageUpload,
|
||||
}),
|
||||
}),
|
||||
(n) => withRenderer(n, { renderer: { type: 'markdown' } }),
|
||||
withExtraProps,
|
||||
),
|
||||
)
|
||||
@@ -353,7 +310,7 @@ export function appComponent(id: string, rendererKey: string) {
|
||||
withFix,
|
||||
withEnabled,
|
||||
(n) => withValue(n, stringValueBehavior),
|
||||
(n) => withComponent(n, { rendererKey }),
|
||||
(n) => withRenderer(n, { renderer: { type: 'custom', key: rendererKey } }),
|
||||
withExtraProps,
|
||||
)
|
||||
return Object.assign(node, {
|
||||
@@ -364,7 +321,7 @@ export function appComponent(id: string, rendererKey: string) {
|
||||
)
|
||||
return this
|
||||
},
|
||||
props(this: Configurable, fn: (ctx: ComponentNodePropsContext) => Record<string, unknown>) {
|
||||
props(this: Configurable, fn: (ctx: NodePropsContext) => Record<string, unknown>) {
|
||||
this._extraProps = fn
|
||||
return this
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * from './builder'
|
||||
export * from './capabilities'
|
||||
export * from './collect'
|
||||
export * from './context'
|
||||
export * from './factories'
|
||||
export * from './fix'
|
||||
export * from './messages'
|
||||
|
||||
Reference in New Issue
Block a user