mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Documentation Panel (#8118)
Implementing most parts of the Documentation Panel in the new GUI. Known issues: - Links do not work (yet, covered by #7992) - Some pages are entirely empty – I’m investigating. Missing doc sections likely cause this, so we probably want to add some placeholder. - Tags do not look as in design. I tried implementing them correctly but didn't have enough time to finish everything. Some initial implementation is in place, though. I will create a separate task for them. https://github.com/enso-org/enso/assets/6566674/a656ae78-5d4c-45f4-a0a5-e07fa573253e
This commit is contained in:
parent
f2651d58e4
commit
523a32471e
@ -69,3 +69,13 @@ npm run test:e2e -- --debug
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Icons license
|
||||
|
||||
We use two Twemoji SVG icons for our documentation panel, you can find them at:
|
||||
|
||||
- `src/assets/icon-important.svg`
|
||||
- `src/assets/icon-info.svg`
|
||||
|
||||
Twemoji SVG icons are licensed under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/.
|
||||
Copyright 2020 Twitter, Inc and other contributors.
|
||||
|
@ -48,7 +48,6 @@
|
||||
"install": "^0.13.0",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"lib0": "^0.2.85",
|
||||
"lorem-ipsum": "^2.0.8",
|
||||
"magic-string": "^0.30.3",
|
||||
"murmurhash": "^2.0.1",
|
||||
"pinia": "^2.1.6",
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* M PLUS 1 font import. */
|
||||
@import url('https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@500;600&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@500;600;800&display=swap');
|
||||
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
|
11
app/gui2/src/assets/icon-examples.svg
Normal file
11
app/gui2/src/assets/icon-examples.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.1981 14.1473L27.3799 22.6473C29.3027 25.2912 27.4141 29 24.1449 29H7.85509C4.58595 29 2.69733 25.2912 4.62015 22.6473L10.802 14.1473C11.1273 13.7 11.5367 13.3316 12 13.0575V6H11.5C10.6716 6 10 5.32843 10 4.5C10 3.67157 10.6716 3 11.5 3H20.5C21.3284 3 22 3.67157 22 4.5C22 5.32843 21.3284 6 20.5 6H20V13.0575C20.4633 13.3316 20.8727 13.7 21.1981 14.1473Z" fill="#6DA85E" fill-opacity="0.7"/>
|
||||
<g clip-path="url(#clip0_7717_245736)">
|
||||
<path d="M21.1981 14.1473L27.3799 22.6473C29.3027 25.2912 27.4141 29 24.1449 29H7.85509C4.58595 29 2.69733 25.2912 4.62015 22.6473L10.802 14.1473C11.1273 13.7 11.5367 13.3316 12 13.0575V6H11.5C10.6716 6 10 5.32843 10 4.5C10 3.67157 10.6716 3 11.5 3H20.5C21.3284 3 22 3.67157 22 4.5C22 5.32843 21.3284 6 20.5 6H20V13.0575C20.4633 13.3316 20.8727 13.7 21.1981 14.1473Z" fill="#6DA85E"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7717_245736">
|
||||
<rect width="32" height="12" fill="white" transform="translate(0 20)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
4
app/gui2/src/assets/icon-important.svg
Normal file
4
app/gui2/src/assets/icon-important.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle fill="#BE1931" cx="18" cy="32" r="3"/>
|
||||
<path fill="#BE1931" d="M21 24c0 1.657-1.344 3-3 3-1.657 0-3-1.343-3-3V5c0-1.657 1.343-3 3-3 1.656 0 3 1.343 3 3v19z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 239 B |
4
app/gui2/src/assets/icon-info.svg
Normal file
4
app/gui2/src/assets/icon-info.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#3B88C3" d="M0 4c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v28c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V4z"/>
|
||||
<path fill="#FFF" d="M20.512 8.071c0 1.395-1.115 2.573-2.511 2.573-1.333 0-2.511-1.209-2.511-2.573 0-1.271 1.178-2.45 2.511-2.45 1.333.001 2.511 1.148 2.511 2.45zm-4.744 6.728c0-1.488.931-2.481 2.232-2.481 1.302 0 2.232.992 2.232 2.481v11.906c0 1.488-.93 2.48-2.232 2.48s-2.232-.992-2.232-2.48V14.799z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 501 B |
18
app/gui2/src/assets/icon-methods.svg
Normal file
18
app/gui2/src/assets/icon-methods.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_7717_245722" fill="white">
|
||||
<rect x="2" y="2" width="13" height="13" rx="2"/>
|
||||
</mask>
|
||||
<rect x="2" y="2" width="13" height="13" rx="2" stroke="#1F71D3" stroke-width="13" mask="url(#path-1-inside-1_7717_245722)"/>
|
||||
<mask id="path-2-inside-2_7717_245722" fill="white">
|
||||
<rect x="17" y="2" width="13" height="13" rx="2"/>
|
||||
</mask>
|
||||
<rect x="17" y="2" width="13" height="13" rx="2" stroke="#1F71D3" stroke-opacity="0.4" stroke-width="13" mask="url(#path-2-inside-2_7717_245722)"/>
|
||||
<mask id="path-3-inside-3_7717_245722" fill="white">
|
||||
<rect x="2" y="17" width="13" height="13" rx="2"/>
|
||||
</mask>
|
||||
<rect x="2" y="17" width="13" height="13" rx="2" stroke="#1F71D3" stroke-opacity="0.7" stroke-width="13" mask="url(#path-3-inside-3_7717_245722)"/>
|
||||
<mask id="path-4-inside-4_7717_245722" fill="white">
|
||||
<rect x="17" y="17" width="13" height="13" rx="2"/>
|
||||
</mask>
|
||||
<rect x="17" y="17" width="13" height="13" rx="2" stroke="#1F71D3" stroke-opacity="0.6" stroke-width="13" mask="url(#path-4-inside-4_7717_245722)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -3,6 +3,7 @@ import { componentBrowserBindings } from '@/bindings'
|
||||
import { makeComponentList, type Component } from '@/components/ComponentBrowser/component'
|
||||
import { Filtering } from '@/components/ComponentBrowser/filtering'
|
||||
import { Input } from '@/components/ComponentBrowser/input'
|
||||
import { default as DocumentationPanel } from '@/components/DocumentationPanel.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import ToggleIcon from '@/components/ToggleIcon.vue'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
@ -14,8 +15,8 @@ import type { useNavigator } from '@/util/navigator'
|
||||
import type { Opt } from '@/util/opt'
|
||||
import { allRanges } from '@/util/range'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
import { LoremIpsum } from 'lorem-ipsum'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import type { SuggestionId } from 'shared/languageServerTypes/suggestions'
|
||||
import { computed, nextTick, onMounted, ref, watch, type Ref } from 'vue'
|
||||
|
||||
const ITEM_SIZE = 32
|
||||
const TOP_BAR_HEIGHT = 32
|
||||
@ -44,8 +45,10 @@ const transform = computed(() => {
|
||||
const nav = props.navigator
|
||||
const translate = nav.translate
|
||||
const position = translate.add(props.position).scale(nav.scale)
|
||||
const x = Math.round(position.x)
|
||||
const y = Math.round(position.y)
|
||||
|
||||
return `translate(${position.x}px, ${position.y}px) translateY(-100%)`
|
||||
return `translate(${x}px, ${y}px) translateY(-100%)`
|
||||
})
|
||||
|
||||
// === Input and Filtering ===
|
||||
@ -169,9 +172,13 @@ const highlightHeight = computed(() => (selected.value != null ? ITEM_SIZE : 0))
|
||||
const animatedHighlightPosition = useApproach(highlightPosition)
|
||||
const animatedHighlightHeight = useApproach(highlightHeight)
|
||||
|
||||
const selectedSuggestion = computed(() => {
|
||||
const selectedSuggestionId = computed(() => {
|
||||
if (selected.value === null) return null
|
||||
const id = components.value[selected.value]?.suggestionId
|
||||
return components.value[selected.value]?.suggestionId ?? null
|
||||
})
|
||||
|
||||
const selectedSuggestion = computed(() => {
|
||||
const id = selectedSuggestionId.value
|
||||
if (id == null) return null
|
||||
return suggestionDbStore.entries.get(id) ?? null
|
||||
})
|
||||
@ -236,7 +243,20 @@ function updateScroll() {
|
||||
// === Documentation Panel ===
|
||||
|
||||
const docsVisible = ref(true)
|
||||
const docs = new LoremIpsum().generateParagraphs(6)
|
||||
|
||||
const displayedDocs: Ref<Opt<SuggestionId>> = ref(null)
|
||||
const docEntry = computed({
|
||||
get() {
|
||||
return displayedDocs.value
|
||||
},
|
||||
set(value) {
|
||||
displayedDocs.value = value
|
||||
},
|
||||
})
|
||||
|
||||
watch(selectedSuggestionId, (id) => {
|
||||
docEntry.value = id
|
||||
})
|
||||
|
||||
// === Accepting Entry ===
|
||||
|
||||
@ -376,8 +396,8 @@ const handler = componentBrowserBindings.handler({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel docs scrollable" :class="{ hidden: !docsVisible }" @wheel.stop.passive>
|
||||
{{ docs }}
|
||||
<div class="panel docs" :class="{ hidden: !docsVisible }">
|
||||
<DocumentationPanel v-model:selectedEntry="docEntry" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="CBInput">
|
||||
@ -433,7 +453,6 @@ const handler = componentBrowserBindings.handler({
|
||||
width: 406px;
|
||||
clip-path: inset(0 0 0 0 round 20px);
|
||||
transition: clip-path 0.2s;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.docs.hidden {
|
||||
clip-path: inset(0 100% 0 0 round 20px);
|
||||
|
102
app/gui2/src/components/DocumentationPanel.vue
Normal file
102
app/gui2/src/components/DocumentationPanel.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import DocsExamples from '@/components/DocumentationPanel/DocsExamples.vue'
|
||||
import DocsHeader from '@/components/DocumentationPanel/DocsHeader.vue'
|
||||
import DocsList from '@/components/DocumentationPanel/DocsList.vue'
|
||||
import DocsSynopsis from '@/components/DocumentationPanel/DocsSynopsis.vue'
|
||||
import DocsTags from '@/components/DocumentationPanel/DocsTags.vue'
|
||||
import type { Docs, FunctionDocs, Sections, TypeDocs } from '@/components/DocumentationPanel/ir'
|
||||
import { lookupDocumentation, placeholder } from '@/components/DocumentationPanel/ir'
|
||||
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||
import type { SuggestionId } from '@/stores/suggestionDatabase/entry'
|
||||
import { type Opt } from '@/util/opt'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ selectedEntry: Opt<SuggestionId> }>()
|
||||
const emit = defineEmits<{ 'update:selectedEntry': [id: SuggestionId] }>()
|
||||
const db = useSuggestionDbStore()
|
||||
|
||||
const documentation = computed<Docs>(() => {
|
||||
const entry = props.selectedEntry
|
||||
return entry ? lookupDocumentation(db.entries, entry) : placeholder('No suggestion selected.')
|
||||
})
|
||||
|
||||
const sections = computed<Sections>(() => {
|
||||
const docs: Docs = documentation.value
|
||||
const fallback = { tags: [], synopsis: [], examples: [] }
|
||||
return docs.kind === 'Placeholder' ? fallback : docs.sections
|
||||
})
|
||||
|
||||
const methods = computed<FunctionDocs[]>(() => {
|
||||
const docs = documentation.value
|
||||
return docs.kind === 'Module' || docs.kind === 'Type' ? docs.methods : []
|
||||
})
|
||||
|
||||
const constructors = computed<FunctionDocs[]>(() => {
|
||||
const docs = documentation.value
|
||||
return docs.kind === 'Type' ? docs.constructors : []
|
||||
})
|
||||
|
||||
const types = computed<TypeDocs[]>(() => {
|
||||
const docs = documentation.value
|
||||
return docs.kind === 'Module' ? docs.types : []
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="DocumentationPanel scrollable" @wheel.stop.passive>
|
||||
<h1 v-if="documentation.kind === 'Placeholder'">{{ documentation.text }}</h1>
|
||||
<DocsTags v-if="sections.tags.length > 0" :tags="sections.tags" />
|
||||
<div class="sections">
|
||||
<span v-if="sections.synopsis.length == 0">{{ 'No documentation available.' }}</span>
|
||||
<DocsSynopsis :sections="sections.synopsis" />
|
||||
<DocsHeader v-if="types.length > 0" kind="types" label="Types" />
|
||||
<DocsList
|
||||
:items="{ kind: 'Types', items: types }"
|
||||
@linkClicked="emit('update:selectedEntry', $event)"
|
||||
/>
|
||||
<DocsHeader v-if="constructors.length > 0" kind="methods" label="Constructors" />
|
||||
<DocsList
|
||||
:items="{ kind: 'Constructors', items: constructors }"
|
||||
@linkClicked="emit('update:selectedEntry', $event)"
|
||||
/>
|
||||
<DocsHeader v-if="methods.length > 0" kind="methods" label="Methods" />
|
||||
<DocsList
|
||||
:items="{ kind: 'Methods', items: methods }"
|
||||
@linkClicked="emit('update:selectedEntry', $event)"
|
||||
/>
|
||||
<DocsHeader v-if="sections.examples.length > 0" kind="examples" label="Examples" />
|
||||
<DocsExamples :examples="sections.examples" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.DocumentationPanel {
|
||||
--enso-docs-type-name-color: #9640da;
|
||||
--enso-docs-methods-header-color: #1f71d3;
|
||||
--enso-docs-method-name-color: #1f71d3;
|
||||
--enso-docs-types-header-color: #1f71d3;
|
||||
--enso-docs-examples-header-color: #6da85e;
|
||||
--enso-docs-important-background-color: #edefe7;
|
||||
--enso-docs-info-background-color: #e6f1f8;
|
||||
--enso-docs-example-background-color: #e6f1f8;
|
||||
--enso-docs-background-color: #eaeaea;
|
||||
--enso-docs-text-color: rbga(0, 0, 0, 0.6);
|
||||
--enso-docs-tag-background-color: #dcd8d8;
|
||||
--enso-docs-code-background-color: #dddcde;
|
||||
font-family: 'M PLUS 1', DejaVuSansMonoBook, sans-serif;
|
||||
font-size: 11.5px;
|
||||
line-height: 160%;
|
||||
color: var(--enso-docs-text-color);
|
||||
background-color: var(--enso-docs-background-color);
|
||||
padding: 8px 12px 4px 8px;
|
||||
white-space: normal;
|
||||
clip-path: inset(0 0 4px 0);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sections {
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
30
app/gui2/src/components/DocumentationPanel/DocsExamples.vue
Normal file
30
app/gui2/src/components/DocumentationPanel/DocsExamples.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { Example } from '@/components/DocumentationPanel/ir'
|
||||
|
||||
const props = defineProps<{ examples: Example[] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.examples.length > 0">
|
||||
<div v-for="(example, index) in props.examples" :key="index" class="exampleContainer">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="example.body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.exampleContainer {
|
||||
background-color: var(--enso-docs-example-background-color);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.example) {
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
margin: 0.05rem 0.1rem;
|
||||
box-shadow: inset 0 0 1px 0.5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
88
app/gui2/src/components/DocumentationPanel/DocsHeader.vue
Normal file
88
app/gui2/src/components/DocumentationPanel/DocsHeader.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import iconExamples from '@/assets/icon-examples.svg'
|
||||
import iconMethods from '@/assets/icon-methods.svg'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export type Kind = 'types' | 'methods' | 'examples'
|
||||
|
||||
const props = defineProps<{ kind: Kind; label: string }>()
|
||||
|
||||
const classes = computed(() => ['Header', props.kind])
|
||||
|
||||
const iconMap: Record<Kind, string> = {
|
||||
types: iconMethods,
|
||||
methods: iconMethods,
|
||||
examples: iconExamples,
|
||||
}
|
||||
|
||||
const icon = computed(() => {
|
||||
return iconMap[props.kind] || iconMethods
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div class="headerIcon">
|
||||
<img :src="icon" />
|
||||
</div>
|
||||
<div class="headerText">{{ props.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.Header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 16px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.headerIcon {
|
||||
display: flex;
|
||||
padding-top: 4px;
|
||||
align-items: flex-start;
|
||||
|
||||
> img {
|
||||
pointer-events: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerText {
|
||||
padding-top: 2px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.methods {
|
||||
color: var(--enso-docs-methods-header-color);
|
||||
}
|
||||
|
||||
.types {
|
||||
color: var(--enso-docs-types-header-color);
|
||||
}
|
||||
|
||||
.examples {
|
||||
color: var(--enso-docs-examples-header-color);
|
||||
|
||||
.headerIcon {
|
||||
padding-top: 3px;
|
||||
|
||||
> img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerText {
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
121
app/gui2/src/components/DocumentationPanel/DocsList.vue
Normal file
121
app/gui2/src/components/DocumentationPanel/DocsList.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import type { FunctionDocs, TypeDocs } from '@/components/DocumentationPanel/ir'
|
||||
import type { Doc } from '@/util/docParser'
|
||||
import type { SuggestionEntryArgument, SuggestionId } from 'shared/languageServerTypes/suggestions'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ items: ListItems }>()
|
||||
const emit = defineEmits<{ linkClicked: [id: SuggestionId] }>()
|
||||
|
||||
interface Methods {
|
||||
kind: 'Methods'
|
||||
items: FunctionDocs[]
|
||||
}
|
||||
|
||||
interface Constructors {
|
||||
kind: 'Constructors'
|
||||
items: FunctionDocs[]
|
||||
}
|
||||
|
||||
interface Types {
|
||||
kind: 'Types'
|
||||
items: TypeDocs[]
|
||||
}
|
||||
|
||||
type ListItems = Methods | Constructors | Types
|
||||
|
||||
function firstParagraph(synopsis: Doc.Section[]): string | undefined {
|
||||
if (synopsis[0] && 'Paragraph' in synopsis[0]) {
|
||||
return synopsis[0].Paragraph.body
|
||||
}
|
||||
}
|
||||
|
||||
function argumentsList(args: SuggestionEntryArgument[]): string {
|
||||
return args
|
||||
.map((arg) => {
|
||||
const defaultValue = arg.defaultValue ? ` = ${arg.defaultValue}` : ''
|
||||
return `${arg.name}${defaultValue}`
|
||||
})
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
const annotations = computed<Array<string | undefined>>(() => {
|
||||
return props.items.items.map((item) => firstParagraph(item.sections.synopsis))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul v-if="props.items.items.length > 0">
|
||||
<li v-for="(item, index) in props.items.items" :key="index" :class="props.items.kind">
|
||||
<a
|
||||
:class="['link', props.items.kind]"
|
||||
@pointerdown.stop.prevent="emit('linkClicked', item.id)"
|
||||
>
|
||||
<span class="entryName">{{ item.name }}</span>
|
||||
<span class="arguments">{{ ' ' + argumentsList(item.arguments) }}</span>
|
||||
</a>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span v-if="annotations[index]" v-html="' ' + annotations[index]"></span>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.link {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.Types {
|
||||
color: var(--enso-docs-type-name-color);
|
||||
}
|
||||
|
||||
&.Methods {
|
||||
color: var(--enso-docs-method-name-color);
|
||||
}
|
||||
|
||||
&.Constructors {
|
||||
color: var(--enso-docs-type-name-color);
|
||||
}
|
||||
}
|
||||
|
||||
.entryName {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.arguments {
|
||||
opacity: 0.34;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
li {
|
||||
&:before {
|
||||
content: '•';
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
&.Types:before {
|
||||
color: var(--enso-docs-type-name-color);
|
||||
}
|
||||
|
||||
&.Methods:before {
|
||||
color: var(--enso-docs-method-name-color);
|
||||
}
|
||||
|
||||
&.Constructors:before {
|
||||
color: var(--enso-docs-method-name-color);
|
||||
}
|
||||
}
|
||||
</style>
|
106
app/gui2/src/components/DocumentationPanel/DocsSynopsis.vue
Normal file
106
app/gui2/src/components/DocumentationPanel/DocsSynopsis.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import iconImportant from '@/assets/icon-important.svg'
|
||||
import iconInfo from '@/assets/icon-info.svg'
|
||||
import type { Doc } from '@/util/docParser'
|
||||
|
||||
const props = defineProps<{ sections: Doc.Section[] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div v-if="props.sections.length > 0">
|
||||
<template v-for="(section, _i) in props.sections" :key="_i">
|
||||
<p v-if="'Paragraph' in section" class="paragraph">
|
||||
<span v-html="section.Paragraph.body"></span>
|
||||
</p>
|
||||
<p v-else-if="'Keyed' in section" class="paragraph">
|
||||
{{ section.Keyed.key + ': ' + section.Keyed.body }}
|
||||
</p>
|
||||
<div v-else-if="'Marked' in section" :class="[section.Marked.mark, 'markedContainer']">
|
||||
<div v-if="'header' in section.Marked" class="markedHeader">
|
||||
<img :src="section.Marked.mark == 'Info' ? iconInfo : iconImportant" class="markedIcon" />
|
||||
{{ section.Marked.header }}
|
||||
</div>
|
||||
<p class="paragraph" v-html="section.Marked.body" />
|
||||
</div>
|
||||
<ul v-else-if="'List' in section">
|
||||
<li v-for="(item, index) in section.List.items" :key="index" v-html="item"></li>
|
||||
</ul>
|
||||
<ul v-else-if="'Arguments' in section">
|
||||
<li v-for="(arg, index) in section.Arguments.args" :key="index">
|
||||
<span class="argument">{{ arg.name }}</span
|
||||
>:
|
||||
<span v-html="arg.description"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
li:before {
|
||||
content: '•';
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
margin: 0;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
.argument {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* Marked sections, such as `Info` and `Important` sections. */
|
||||
|
||||
.Info {
|
||||
background-color: var(--enso-docs-info-background-color);
|
||||
}
|
||||
|
||||
.Important {
|
||||
background-color: var(--enso-docs-important-background-color);
|
||||
}
|
||||
|
||||
.markedContainer {
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.markedHeader {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.markedIcon {
|
||||
pointer-events: none;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin: 0 0.05em 0 0.05em;
|
||||
vertical-align: -0.1em;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
/* Code. The words emphasized with backticks. */
|
||||
|
||||
:deep(code) {
|
||||
background-color: var(--enso-docs-code-background-color);
|
||||
border-radius: 4px;
|
||||
padding: 3px;
|
||||
}
|
||||
</style>
|
66
app/gui2/src/components/DocumentationPanel/DocsTags.vue
Normal file
66
app/gui2/src/components/DocumentationPanel/DocsTags.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import type { Doc } from '@/util/docParser'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ tags: Doc.Section.Tag[] }>()
|
||||
|
||||
// Split Alias tags into separate ones.
|
||||
const tags = computed<Doc.Section.Tag[]>(() => {
|
||||
return props.tags.flatMap((tag) => {
|
||||
if (tag.tag === 'Alias') {
|
||||
return tag.body.split(/,\s*/).map((body) => ({ tag: 'Alias', body }))
|
||||
} else {
|
||||
return tag
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
interface View {
|
||||
label: string
|
||||
class?: string
|
||||
style?: { [key: string]: string | number | undefined }
|
||||
}
|
||||
|
||||
const defaultView = (tag: Doc.Tag, body: Doc.HtmlString): View => ({
|
||||
label: tag + (body.length > 0 ? `=${body}` : ''),
|
||||
})
|
||||
const tagsMap: Partial<Record<Doc.Tag, (body: Doc.HtmlString) => View>> = {
|
||||
Alias: (body) => ({ label: body }),
|
||||
}
|
||||
const view = (tag: Doc.Section.Tag): View => {
|
||||
const maybeView = tagsMap[tag.tag]
|
||||
return maybeView ? maybeView(tag.body) : defaultView(tag.tag, tag.body)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="tags.length > 0" class="Tags">
|
||||
<div
|
||||
v-for="(tag, index) in tags"
|
||||
:key="index"
|
||||
:class="view(tag).class || 'tag'"
|
||||
:style="view(tag).style || {}"
|
||||
>
|
||||
{{ view(tag).label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.Tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
height: 24px;
|
||||
align-items: flex-start;
|
||||
background-color: var(--enso-docs-tag-background-color);
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
</style>
|
179
app/gui2/src/components/DocumentationPanel/ir.ts
Normal file
179
app/gui2/src/components/DocumentationPanel/ir.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import type { SuggestionDb } from '@/stores/suggestionDatabase'
|
||||
import type { SuggestionEntry, SuggestionId } from '@/stores/suggestionDatabase/entry'
|
||||
import { SuggestionKind } from '@/stores/suggestionDatabase/entry'
|
||||
import type { Doc } from '@/util/docParser'
|
||||
import type { SuggestionEntryArgument } from 'shared/languageServerTypes/suggestions'
|
||||
|
||||
// === Types ===
|
||||
|
||||
/**
|
||||
* Intermediate representation of the entries documentation.
|
||||
*/
|
||||
|
||||
export type Docs = FunctionDocs | TypeDocs | ModuleDocs | LocalDocs | Placeholder
|
||||
|
||||
export interface Placeholder {
|
||||
kind: 'Placeholder'
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface FunctionDocs {
|
||||
kind: 'Function'
|
||||
id: SuggestionId
|
||||
name: string
|
||||
arguments: SuggestionEntryArgument[]
|
||||
sections: Sections
|
||||
}
|
||||
|
||||
export interface TypeDocs {
|
||||
kind: 'Type'
|
||||
id: SuggestionId
|
||||
name: string
|
||||
arguments: SuggestionEntryArgument[]
|
||||
sections: Sections
|
||||
methods: FunctionDocs[]
|
||||
constructors: FunctionDocs[]
|
||||
}
|
||||
|
||||
export interface ModuleDocs {
|
||||
kind: 'Module'
|
||||
id: SuggestionId
|
||||
name: string
|
||||
sections: Sections
|
||||
types: TypeDocs[]
|
||||
methods: FunctionDocs[]
|
||||
}
|
||||
|
||||
export interface LocalDocs {
|
||||
kind: 'Local'
|
||||
id: SuggestionId
|
||||
name: string
|
||||
sections: Sections
|
||||
}
|
||||
|
||||
// === Sections ===
|
||||
|
||||
export interface Example {
|
||||
header?: string
|
||||
body: Doc.HtmlString
|
||||
}
|
||||
|
||||
export function placeholder(text: string): Placeholder {
|
||||
return { kind: 'Placeholder', text }
|
||||
}
|
||||
|
||||
/**
|
||||
* Documentation sections split into three categories.
|
||||
*
|
||||
* These categories of sections are present in almost every documentation page.
|
||||
*/
|
||||
export interface Sections {
|
||||
tags: Doc.Section.Tag[]
|
||||
synopsis: Doc.Section[]
|
||||
examples: Example[]
|
||||
}
|
||||
|
||||
// Split doc sections into categories.
|
||||
function filterSections(sections: Iterable<Doc.Section>): Sections {
|
||||
const tags = []
|
||||
const synopsis = []
|
||||
const examples = []
|
||||
|
||||
for (const section of sections) {
|
||||
const isTag = 'Tag' in section
|
||||
const isExample = 'Marked' in section && section.Marked.mark === 'Example'
|
||||
if (isTag) {
|
||||
tags.push(section.Tag)
|
||||
} else if (isExample) {
|
||||
examples.push({ ...section.Marked } as Example)
|
||||
} else {
|
||||
synopsis.push(section)
|
||||
}
|
||||
}
|
||||
return { tags, synopsis, examples }
|
||||
}
|
||||
|
||||
// === Lookup ===
|
||||
|
||||
export function lookupDocumentation(db: SuggestionDb, id: SuggestionId): Docs {
|
||||
const entry = db.get(id)
|
||||
if (!entry)
|
||||
return placeholder(
|
||||
`Documentation not available. Entry with id ${id} not found in the database.`,
|
||||
)
|
||||
const handle = handleDocumentation[entry.kind]
|
||||
return handle ? handle(db, entry, id) : placeholder(`Entry kind ${entry.kind} was not handled.`)
|
||||
}
|
||||
|
||||
function getChildren(db: SuggestionDb, id: SuggestionId, kind: SuggestionKind): Docs[] {
|
||||
if (!id) return []
|
||||
const children = Array.from(db.parent.reverseLookup(id))
|
||||
return children.reduce((acc: Docs[], id: SuggestionId) => {
|
||||
if (db.get(id)?.kind === kind) {
|
||||
const docs = lookupDocumentation(db, id)
|
||||
acc.push(docs)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
function asFunctionDocs(docs: Docs[]): FunctionDocs[] {
|
||||
return docs.flatMap((doc) => {
|
||||
if (doc.kind === 'Function') {
|
||||
return [doc]
|
||||
} else {
|
||||
console.error(`Unexpected docs type: ${docs}, expected Function`)
|
||||
return []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function asTypeDocs(docs: Docs[]): TypeDocs[] {
|
||||
return docs.flatMap((doc) => {
|
||||
if (doc.kind === 'Type') {
|
||||
return [doc]
|
||||
} else {
|
||||
console.error(`Unexpected docs type: ${docs}, expected Type`)
|
||||
return []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type DocsHandle = (db: SuggestionDb, entry: SuggestionEntry, id: SuggestionId) => Docs
|
||||
|
||||
const handleFunction: DocsHandle = (_db, entry, id) => ({
|
||||
kind: 'Function',
|
||||
id,
|
||||
name: entry.name,
|
||||
arguments: entry.arguments,
|
||||
sections: filterSections(entry.documentation),
|
||||
})
|
||||
|
||||
const handleDocumentation: Record<SuggestionKind, DocsHandle> = {
|
||||
[SuggestionKind.Function]: handleFunction,
|
||||
[SuggestionKind.Method]: handleFunction,
|
||||
[SuggestionKind.Constructor]: handleFunction,
|
||||
[SuggestionKind.Local]: (_db, entry, id) => ({
|
||||
kind: 'Local',
|
||||
id,
|
||||
name: entry.name,
|
||||
sections: filterSections(entry.documentation),
|
||||
}),
|
||||
[SuggestionKind.Type]: (db, entry, id) => ({
|
||||
kind: 'Type',
|
||||
id,
|
||||
name: entry.name,
|
||||
arguments: entry.arguments,
|
||||
sections: filterSections(entry.documentation),
|
||||
methods: asFunctionDocs(getChildren(db, id, SuggestionKind.Method)),
|
||||
constructors: asFunctionDocs(getChildren(db, id, SuggestionKind.Constructor)),
|
||||
}),
|
||||
[SuggestionKind.Module]: (db, entry, id) => ({
|
||||
kind: 'Module',
|
||||
id,
|
||||
name: entry.name,
|
||||
sections: filterSections(entry.documentation),
|
||||
types: asTypeDocs(getChildren(db, id, SuggestionKind.Type)),
|
||||
methods: asFunctionDocs(getChildren(db, id, SuggestionKind.Method)),
|
||||
}),
|
||||
}
|
@ -417,15 +417,15 @@ const activeStyle = computed(() => {
|
||||
if (base.value == null) return {}
|
||||
if (navigator?.sceneMousePos == null) return {}
|
||||
const length = base.value.getTotalLength()
|
||||
let offset = lengthTo(navigator?.sceneMousePos)
|
||||
let offset = lengthTo(navigator.sceneMousePos)
|
||||
if (offset == null) return {}
|
||||
offset = length - offset
|
||||
if (offset < length / 2) {
|
||||
offset += length
|
||||
}
|
||||
return {
|
||||
'stroke-dasharray': length,
|
||||
'stroke-dashoffset': offset,
|
||||
strokeDasharray: length,
|
||||
strokeDashoffset: offset,
|
||||
}
|
||||
})
|
||||
|
||||
@ -459,22 +459,23 @@ const arrowTransform = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="basePath">
|
||||
<path
|
||||
v-if="basePath"
|
||||
:d="basePath"
|
||||
class="edge io"
|
||||
@pointerdown="click"
|
||||
@pointerenter="hovered = true"
|
||||
@pointerleave="hovered = false"
|
||||
/>
|
||||
<path v-if="basePath" ref="base" :d="basePath" class="edge visible base" />
|
||||
<path v-if="activePath" :d="activePath" class="edge visible active" :style="activeStyle" />
|
||||
<path ref="base" :d="basePath" class="edge visible base" />
|
||||
<polygon
|
||||
v-if="arrowTransform"
|
||||
:transform="arrowTransform"
|
||||
points="0,-9.375 -9.375,9.375 9.375,9.375"
|
||||
class="arrow visible"
|
||||
/>
|
||||
<path v-if="activePath" :d="activePath" class="edge visible active" :style="activeStyle" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@ -486,6 +487,7 @@ const arrowTransform = computed(() => {
|
||||
}
|
||||
.edge {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.edge.io {
|
||||
stroke-width: 14;
|
||||
|
@ -7,6 +7,7 @@ import { injectGraphSelection } from '@/providers/graphSelection'
|
||||
import type { Node } from '@/stores/graph'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||
import { useApproach } from '@/util/animation'
|
||||
import { colorFromString } from '@/util/colors'
|
||||
import { usePointer, useResizeObserver } from '@/util/events'
|
||||
import { methodNameToIcon, typeNameToIcon } from '@/util/getIconName'
|
||||
@ -65,6 +66,17 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const outputHovered = ref(false)
|
||||
const hoverAnimation = useApproach(() => (outputHovered.value ? 1 : 0), 50, 0.01)
|
||||
|
||||
const bgStyleVariables = computed(() => {
|
||||
return {
|
||||
'--node-width': `${nodeSize.value.x}px`,
|
||||
'--node-height': `${nodeSize.value.y}px`,
|
||||
'--hover-animation': `${hoverAnimation.value}`,
|
||||
}
|
||||
})
|
||||
|
||||
const transform = computed(() => {
|
||||
let pos = props.node.position
|
||||
return `translate(${pos.x}px, ${pos.y}px)`
|
||||
@ -420,81 +432,98 @@ function hoverExpr(id: ExprId | undefined) {
|
||||
@updateHoveredExpr="hoverExpr($event)"
|
||||
/></span>
|
||||
</div>
|
||||
<div key="outputPort" class="outputPort" @pointerdown="emit('outputPortAction')">
|
||||
<svg viewBox="-22 -35 22 38" xmlns="http://www.w3.org/2000/svg" class="outputPortCap">
|
||||
<path d="M 0 0 a 19 19 0 0 1 -19 -19" class="outputPortCapLine" />
|
||||
<rect height="6" width="6" x="0" y="-3" class="outputPortCapButt" />
|
||||
</svg>
|
||||
<svg
|
||||
viewBox="0 -35 1 38"
|
||||
preserveAspectRatio="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="outputPortBar"
|
||||
>
|
||||
<path d="M 0 0 h 1" class="outputPortBarLine" />
|
||||
</svg>
|
||||
<svg viewBox="0 -35 22 38" xmlns="http://www.w3.org/2000/svg" class="outputPortCap">
|
||||
<path d="M 0 0 a 19 19 0 0 0 19 -19" class="outputPortCapLine" />
|
||||
<rect height="6" width="6" x="-6" y="-3" class="outputPortCapButt" />
|
||||
</svg>
|
||||
<svg class="bgPaths" :style="bgStyleVariables">
|
||||
<rect class="bgFill" />
|
||||
<rect
|
||||
class="outputPortHoverArea"
|
||||
@pointerenter="outputHovered = true"
|
||||
@pointerleave="outputHovered = false"
|
||||
@pointerdown="emit('outputPortAction')"
|
||||
/>
|
||||
</div>
|
||||
<rect class="outputPort" />
|
||||
</svg>
|
||||
<div class="outputTypeName">{{ outputTypeName }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.outputPort {
|
||||
.bgPaths {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
|
||||
--output-port-max-width: 6px;
|
||||
--output-port-overlap: 0.1px;
|
||||
--output-port-hover-width: 8px;
|
||||
}
|
||||
.outputPort:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.outputPortCap {
|
||||
flex: none;
|
||||
height: 38px;
|
||||
width: 22px;
|
||||
}
|
||||
.outputPortCapLine {
|
||||
.outputPort,
|
||||
.outputPortHoverArea {
|
||||
x: calc(0px - var(--output-port-width) / 2);
|
||||
y: calc(0px - var(--output-port-width) / 2);
|
||||
width: calc(var(--node-width) + var(--output-port-width));
|
||||
height: calc(var(--node-height) + var(--output-port-width));
|
||||
rx: calc(var(--node-border-radius) + var(--output-port-width) / 2);
|
||||
|
||||
fill: none;
|
||||
stroke: var(--node-group-color);
|
||||
opacity: 30%;
|
||||
stroke-width: 6px;
|
||||
stroke: var(--node-color-port);
|
||||
stroke-width: calc(var(--output-port-width) + var(--output-port-overlap));
|
||||
transition: stroke 0.2s ease;
|
||||
--horizontal-line: calc(var(--node-width) - var(--node-border-radius) * 2);
|
||||
--vertical-line: calc(var(--node-height) - var(--node-border-radius) * 2);
|
||||
--radius-arclength: calc(
|
||||
(var(--node-border-radius) + var(--output-port-width) * 0.5) * 2 * 3.141592653589793
|
||||
);
|
||||
|
||||
stroke-dasharray: calc(var(--horizontal-line) + var(--radius-arclength) * 0.5) 10000%;
|
||||
stroke-dashoffset: calc(
|
||||
0px - var(--horizontal-line) - var(--vertical-line) - var(--radius-arclength) * 0.25
|
||||
);
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.outputPortCapButt {
|
||||
fill: var(--node-group-color);
|
||||
opacity: 30%;
|
||||
|
||||
.outputPort {
|
||||
--output-port-width: calc(
|
||||
var(--output-port-max-width) * var(--hover-animation) - var(--output-port-overlap)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
.outputPortBar {
|
||||
height: 38px;
|
||||
width: 100%;
|
||||
|
||||
.outputPortHoverArea {
|
||||
--output-port-width: var(--output-port-hover-width);
|
||||
stroke: transparent;
|
||||
pointer-events: all;
|
||||
}
|
||||
.outputPortBarLine {
|
||||
fill: none;
|
||||
stroke: var(--node-group-color);
|
||||
opacity: 30%;
|
||||
/* 6px + extra width to prevent antialiasing issues:
|
||||
The 1px on the top will draw mostly under the node, but will ensure the line meets the node.
|
||||
(The 1px on the bottom will be clipped.) */
|
||||
stroke-width: 8px;
|
||||
|
||||
.bgFill {
|
||||
width: var(--node-width);
|
||||
height: var(--node-height);
|
||||
rx: var(--node-border-radius);
|
||||
|
||||
fill: var(--node-color-primary);
|
||||
transition: fill 0.2s ease;
|
||||
}
|
||||
|
||||
.bgPaths .bgPaths:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.GraphNode {
|
||||
--node-height: 32px;
|
||||
--node-border-radius: calc(var(--node-height) * 0.5);
|
||||
--output-port-padding: 6px;
|
||||
--node-border-radius: 16px;
|
||||
|
||||
--node-group-color: #357ab9;
|
||||
|
||||
--node-color-primary: color-mix(in oklab, var(--node-group-color) 100%, transparent 0%);
|
||||
--node-color-primary: color-mix(
|
||||
in oklab,
|
||||
var(--node-group-color) 100%,
|
||||
var(--node-group-color) 0%
|
||||
);
|
||||
--node-color-port: color-mix(in oklab, var(--node-color-primary) 75%, white 15%);
|
||||
--node-color-error: color-mix(in oklab, var(--node-group-color) 30%, rgba(255, 0, 0) 70%);
|
||||
--node-color-error: color-mix(in oklab, var(--node-group-color) 30%, rgb(255, 0, 0) 70%);
|
||||
|
||||
&.executionState-Unknown,
|
||||
&.executionState-Pending {
|
||||
@ -507,9 +536,6 @@ function hoverExpr(id: ExprId | undefined) {
|
||||
::selection {
|
||||
background-color: rgba(255, 255, 255, 20%);
|
||||
}
|
||||
|
||||
padding-left: var(--output-port-padding);
|
||||
padding-right: var(--output-port-padding);
|
||||
}
|
||||
|
||||
.node {
|
||||
@ -518,8 +544,6 @@ function hoverExpr(id: ExprId | undefined) {
|
||||
left: 0;
|
||||
caret-shape: bar;
|
||||
height: var(--node-height);
|
||||
background: var(--node-color-primary);
|
||||
background-clip: padding-box;
|
||||
border-radius: var(--node-border-radius);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -527,14 +551,12 @@ function hoverExpr(id: ExprId | undefined) {
|
||||
white-space: nowrap;
|
||||
padding: 4px 8px;
|
||||
z-index: 2;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
outline 0.2s ease;
|
||||
transition: outline 0.2s ease;
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
.GraphNode .selection {
|
||||
position: absolute;
|
||||
inset: calc(0px - var(--selected-node-border-width) + var(--output-port-padding));
|
||||
inset: calc(0px - var(--selected-node-border-width));
|
||||
--node-current-selection-width: 0px;
|
||||
|
||||
&:before {
|
||||
|
@ -118,7 +118,7 @@ function hover(part: 'tree' | 'token', isHovered: boolean) {
|
||||
v-else
|
||||
ref="rootNode"
|
||||
:class="['Tree', spanClass]"
|
||||
:data-span-start="props.ast.span()[0] - nodeSpanStart"
|
||||
:data-span-start="props.ast.span()[0] - nodeSpanStart - whitespace.length"
|
||||
>{{ whitespace
|
||||
}}<template v-for="child in children" :key="child.astId">
|
||||
<NodeTree
|
||||
|
24
package-lock.json
generated
24
package-lock.json
generated
@ -45,7 +45,6 @@
|
||||
"install": "^0.13.0",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"lib0": "^0.2.85",
|
||||
"lorem-ipsum": "^2.0.8",
|
||||
"magic-string": "^0.30.3",
|
||||
"murmurhash": "^2.0.1",
|
||||
"pinia": "^2.1.6",
|
||||
@ -11329,29 +11328,6 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lorem-ipsum": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-2.0.8.tgz",
|
||||
"integrity": "sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA==",
|
||||
"dependencies": {
|
||||
"commander": "^9.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"lorem-ipsum": "dist/bin/lorem-ipsum.bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.x",
|
||||
"npm": ">= 5.x"
|
||||
}
|
||||
},
|
||||
"node_modules/lorem-ipsum/node_modules/commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "2.3.6",
|
||||
"dev": true,
|
||||
|
Loading…
Reference in New Issue
Block a user