Skip to content

Commit 28b6dd1

Browse files
committed
feat: add documentation and dev resource links
1 parent b88831b commit 28b6dd1

File tree

5 files changed

+298
-235
lines changed

5 files changed

+298
-235
lines changed

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,18 +136,16 @@ See [Justineo/tempad-dev-plugin-kong](https://github.com/Justineo/tempad-dev-plu
136136
137137
Currently, we support 4 plugin hooks:
138138

139-
- `transform`: Adjusts the generated CSS string or parsed style object before it is rendered in the panel.
140-
- `transformVariable`: Remaps CSS variables, letting you emit alternate token syntaxes such as Sass variables.
141-
- `transformPx`: Rewrites numeric pixel values while respecting user preferences like `useRem` and `rootFontSize`.
142-
- `transformComponent`: Converts the inspected component instance into either a `DevComponent` tree or a preformatted string for the code block.
139+
- `transform`: Converts the style object or code into a string format for the code block. Useful for custom structures, such as Tailwind CSS or UnoCSS.
140+
- `transformVariable`: Converts CSS variables into alternate formats, e.g., converting them to Sass variables for design tokens.
141+
- `transformPx`: Converts pixel values into other units or scales.
142+
- `transformComponent`: Converts the design component object into a dev component object or a strin for the code block. Useful for generating component code for design systems.
143143

144144
> [!TIP]
145145
> To include JavaScript variables in generated CSS, wrap the variable name in `\0` characters. This will convert it into string interpolation for JavaScript.
146146
> e.g. if you return `\0foo\0` as the return value, an input of `calc(var(--foo) + 10px)` will be transformed into a JavaScript template string as `` `calc(${foo} + 10px)` ``.
147147
148-
Additionally, you can specify a custom `title` and `lang` for the code block (supported values include `text`, `tsx`, `jsx`, `ts`, `js`, `vue`, `html`, `css`, `sass`, `scss`, `less`, `stylus`, and `json`) or hide the built-in code block by setting it to `false`.
149-
150-
When `transformComponent` returns a `DevComponent` tree, TemPad Dev serializes it to JSX by default. Set `lang` to `'vue'` to render Vue template markup, or return a string directly if you need a bespoke serialization strategy. The exported `h` helper from `@tempad-dev/plugins` builds `DevComponent` trees with concise hyperscript-style calls.
148+
Additionally, you can specify a custom `title` and `lang` for the code block or hide the built-in code block by setting it to `false`.
151149

152150
For full type definitions and helper functions, see [`plugins/src/index.ts`](./plugins/src/index.ts).
153151

components/sections/MetaSection.vue

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import Copyable from '@/components/Copyable.vue'
44
import IconButton from '@/components/IconButton.vue'
55
import Select from '@/components/icons/Select.vue'
66
import Section from '@/components/Section.vue'
7-
import { selection, selectedTemPadComponent } from '@/ui/state'
7+
import { useDevResourceLinks } from '@/composables/dev-resources'
8+
import { selection, selectedNode, selectedTemPadComponent } from '@/ui/state'
9+
import Link from '@/components/icons/Link.vue'
810
911
const title = computed(() => {
1012
const nodes = selection.value
@@ -29,6 +31,8 @@ const showFocusButton = computed(
2931
() => window.figma && selection.value && selection.value.length > 0
3032
)
3133
34+
const devResourceLinks = useDevResourceLinks(selectedNode)
35+
3236
const libDisplayName = computed(() => selectedTemPadComponent.value?.libDisplayName)
3337
3438
const libName = computed(() => selectedTemPadComponent.value?.libName)
@@ -42,15 +46,19 @@ function scrollIntoView() {
4246
<template>
4347
<Section flat>
4448
<template #header>
45-
<span class="tp-meta-title-aux tp-ellipsis" v-if="title == null">No selection</span>
46-
<div class="tp-row tp-shrink tp-gap-l" v-else>
47-
<Copyable class="tp-ellipsis">
49+
<div class="tp-meta-title tp-row tp-shrink tp-gap-l" v-if="title != null">
50+
<Copyable
51+
class="tp-ellipsis"
52+
:data-tooltip="`Click to copy layer name: ${title}`"
53+
data-tooltip-type="text"
54+
>
4855
{{ title }}
4956
</Copyable>
5057
<Copyable variant="block" :data-copy="libName">
5158
<Badge v-if="libName" :title="libName">{{ libDisplayName || libName }}</Badge>
5259
</Copyable>
5360
</div>
61+
<span class="tp-meta-title-aux tp-ellipsis" v-else>No selection</span>
5462
<IconButton
5563
v-if="showFocusButton"
5664
title="Scroll into view"
@@ -60,15 +68,82 @@ function scrollIntoView() {
6068
<Select />
6169
</IconButton>
6270
</template>
71+
<div v-if="devResourceLinks.length > 0" class="tp-meta-dev-links">
72+
<a
73+
class="tp-row tp-meta-dev-link"
74+
:href="link.url"
75+
target="_blank"
76+
v-for="link in devResourceLinks"
77+
:key="link.url"
78+
>
79+
<div class="tp-meta-dev-link-icon">
80+
<img class="tp-meta-dev-link-favicon" v-if="link.favicon" :src="link.favicon" />
81+
<Link class="tp-meta-dev-link-fallback-icon" v-else />
82+
</div>
83+
<span class="tp-meta-dev-link-label">{{ link.name }}</span>
84+
</a>
85+
</div>
6386
</Section>
6487
</template>
6588

6689
<style scoped>
67-
.tp-meta-title:not(:hover) .tp-meta-scroll {
68-
display: none;
90+
.tp-meta-title {
91+
font-family: var(--text-body-large-strong-font-family);
92+
font-size: var(--text-body-large-strong-font-size);
93+
font-weight: var(--text-body-large-strong-font-weight);
94+
letter-spacing: var(--text-body-large-strong-letter-spacing);
95+
line-height: var(--text-body-large-strong-line-height);
96+
color: var(--color-text);
6997
}
7098
7199
.tp-meta-title-aux {
72100
color: var(--color-text-secondary);
73101
}
102+
103+
.tp-meta-dev-links {
104+
display: flex;
105+
flex-direction: column;
106+
gap: 4px;
107+
padding-bottom: 12px;
108+
}
109+
110+
.tp-meta-dev-link {
111+
position: relative;
112+
font-family: var(--text-body-medium-font-family);
113+
font-size: var(--text-body-medium-font-size);
114+
font-weight: var(--text-body-medium-font-weight);
115+
letter-spacing: var(--text-body-medium-letter-spacing);
116+
line-height: var(--text-body-medium-line-height);
117+
height: 24px;
118+
gap: 4px;
119+
overflow-wrap: break-word;
120+
cursor: pointer;
121+
}
122+
123+
.tp-meta-dev-link:hover::after {
124+
content: '';
125+
position: absolute;
126+
inset: 0 calc(var(--spacer-2, 0.5rem) * -1);
127+
background: var(--color-bghovertransparent);
128+
border-radius: var(--fpl-radius-left, var(--radius-medium))
129+
var(--fpl-radius-right, var(--radius-medium)) var(--fpl-radius-right, var(--radius-medium))
130+
var(--fpl-radius-left, var(--radius-medium));
131+
}
132+
133+
.tp-meta-dev-link-label {
134+
color: var(--color-text);
135+
overflow: hidden;
136+
text-overflow: ellipsis;
137+
white-space: nowrap;
138+
}
139+
140+
.tp-meta-dev-link-favicon {
141+
width: 16px;
142+
height: 16px;
143+
vertical-align: middle;
144+
}
145+
146+
.tp-meta-dev-link-fallback-icon {
147+
margin-inline: -4px;
148+
}
74149
</style>

composables/dev-resources.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { SelectionNode } from '@/ui/state'
2+
3+
const devResourcesCache = reactive<Map<string, DevResourceWithNodeId[]>>(new Map())
4+
const inflightDevResources = new Map<string, Promise<void>>()
5+
6+
const faviconCache = reactive<Map<string, string | null>>(new Map())
7+
const inflightFavicons = new Map<string, Promise<void>>()
8+
9+
const FAVICON_PROXY_ENDPOINT = 'https://www.figma.com/api/favicon_for_url_proxy?url='
10+
11+
export type DevResourceLink = {
12+
name: string
13+
url: string
14+
favicon: string | null
15+
inherited: boolean
16+
}
17+
18+
async function getFavicon(url: string) {
19+
try {
20+
const response = await fetch(FAVICON_PROXY_ENDPOINT + encodeURIComponent(url))
21+
if (!response.ok) {
22+
return null
23+
}
24+
const { meta } = await response.json()
25+
return bytesToDataURL(new Uint8Array(meta))
26+
} catch {
27+
return null
28+
}
29+
}
30+
31+
export function useDevResourceLinks(
32+
nodeSource: MaybeRefOrGetter<SelectionNode | null | undefined>
33+
) {
34+
const nodeRef = computed(() => toValue(nodeSource) ?? null)
35+
36+
const links = computed<DevResourceLink[]>(() => {
37+
const node = nodeRef.value
38+
if (!node) {
39+
return []
40+
}
41+
42+
const documentationLinks = getDocumentationLinks(node)
43+
const resources = devResourcesCache.get(node.id) ?? []
44+
45+
const docLinks = documentationLinks.map(({ uri }) => toLink('Documentation', uri))
46+
const resourceLinks = [...resources]
47+
.sort((a, b) => Number(Boolean(b.inheritedNodeId)) - Number(Boolean(a.inheritedNodeId)))
48+
.map(({ name, url, inheritedNodeId }) => toLink(name, url, inheritedNodeId != null))
49+
50+
return [...docLinks, ...resourceLinks]
51+
})
52+
53+
watchEffect(() => {
54+
const node = nodeRef.value
55+
if (!node) {
56+
return
57+
}
58+
59+
ensureDevResources(node)
60+
})
61+
62+
watchEffect(() => {
63+
const node = nodeRef.value
64+
if (!node) {
65+
return
66+
}
67+
68+
for (const { url } of links.value) {
69+
if (url && !faviconCache.has(url)) {
70+
ensureFavicon(url)
71+
}
72+
}
73+
})
74+
75+
return links
76+
}
77+
78+
function ensureDevResources(node: SelectionNode) {
79+
if (inflightDevResources.has(node.id)) {
80+
return
81+
}
82+
83+
async function request() {
84+
try {
85+
const resources = await node.getDevResourcesAsync()
86+
devResourcesCache.set(node.id, resources)
87+
} finally {
88+
inflightDevResources.delete(node.id)
89+
}
90+
}
91+
92+
inflightDevResources.set(node.id, request())
93+
}
94+
95+
function ensureFavicon(url: string) {
96+
if (faviconCache.has(url) || inflightFavicons.has(url)) {
97+
return
98+
}
99+
100+
async function request() {
101+
try {
102+
const favicon = await getFavicon(url)
103+
faviconCache.set(url, favicon)
104+
} finally {
105+
inflightFavicons.delete(url)
106+
}
107+
}
108+
109+
inflightFavicons.set(url, request())
110+
}
111+
112+
function bytesToDataURL(bytes: Uint8Array) {
113+
let binary = ''
114+
const chunkSize = 0x8000
115+
for (let i = 0; i < bytes.length; i += chunkSize) {
116+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize))
117+
}
118+
return `data:image/png;base64,${btoa(binary)}`
119+
}
120+
121+
function toLink(name: string, url: string, inherited: boolean = false): DevResourceLink {
122+
return {
123+
name,
124+
url,
125+
favicon: url ? (faviconCache.get(url) ?? null) : null,
126+
inherited
127+
}
128+
}
129+
130+
function getDocumentationLinks(node: SelectionNode): readonly DocumentationLink[] {
131+
if (node.type !== 'INSTANCE' && node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET') {
132+
return []
133+
}
134+
135+
let currentNode = node
136+
137+
if (currentNode.type === 'INSTANCE' && currentNode.mainComponent) {
138+
currentNode = currentNode.mainComponent
139+
}
140+
141+
if (currentNode.type === 'COMPONENT') {
142+
if (currentNode.documentationLinks.length > 0) {
143+
return currentNode.documentationLinks
144+
} else if (currentNode.parent && currentNode.parent.type === 'COMPONENT_SET') {
145+
return currentNode.parent.documentationLinks
146+
}
147+
}
148+
149+
return []
150+
}

entrypoints/ui/style.css

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,29 +96,6 @@ tempad input[type="number"]:focus-visible {
9696
outline-offset: -2px;
9797
}
9898

99-
tempad ::-webkit-scrollbar {
100-
width: 11px;
101-
height: 11px;
102-
background-color: transparent;
103-
border: 0 solid var(--color-border);
104-
}
105-
106-
tempad ::-webkit-scrollbar:horizontal {
107-
border-top-width: 1px;
108-
}
109-
110-
tempad ::-webkit-scrollbar:vertical {
111-
border-left-width: 1px;
112-
}
113-
114-
tempad ::-webkit-scrollbar-thumb {
115-
border-radius: 8px;
116-
border: solid transparent;
117-
border-width: 3px 2px 2px 3px;
118-
background-clip: content-box;
119-
background-color: var(--color-scrollbar);
120-
}
121-
12299
[data-fpl-version="ui3"] tempad .tp-gap {
123100
gap: var(--spacer-1);
124101
}

0 commit comments

Comments
 (0)