Skip to content

Commit 552faba

Browse files
committed
feat: Setup renderer for vue + button vue component
1 parent 4f9b534 commit 552faba

File tree

12 files changed

+360
-0
lines changed

12 files changed

+360
-0
lines changed

js/showcase/src/vue/ErrorBoundary.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { h as vueH, VNode } from "vue";
2+
import { ErrorsDef, ErrorStack } from "../error";
3+
4+
export function createErrorBoundary(
5+
render: () => VNode,
6+
renderErrors: (error: ErrorsDef) => VNode
7+
): VNode {
8+
return vueH("div", null, render());
9+
// return jsxs(ErrorBoundary, { render, renderErrors });
10+
}
11+
12+
function errorToDef(error: Error): ErrorsDef {
13+
const stack: ErrorStack[] = (error.stack?.split?.("\n") ?? [])
14+
.map(line => {
15+
if (line.length === 0) {
16+
return;
17+
}
18+
19+
// split by really last @ or @http(s)?://
20+
let [, name, source = ""] = line.match(/(.*)@(https?:\/\/.*)$/) ??
21+
line.match(/(.*)@(:?[^@]*)$/) ?? [, line];
22+
23+
// replace empty string to anonymous call
24+
name = name || "<anonymous>";
25+
26+
// Ignore all the internal functions of react and vite
27+
if (
28+
source.startsWith("vite/client") ||
29+
source.startsWith("react-refresh") ||
30+
name.includes("/node_modules/") ||
31+
name.startsWith("__require")
32+
) {
33+
return;
34+
}
35+
36+
// Remove any URL prefix, leave just path
37+
source = source.startsWith(location.origin)
38+
? source.substring(location.origin.length)
39+
: source;
40+
41+
// Remove node_modules prefix from URL
42+
const NODE_MODULES_DEPS = "/node_modules/.vite/deps/";
43+
source = source.startsWith(NODE_MODULES_DEPS)
44+
? "/node_modules/" +
45+
source.substring(NODE_MODULES_DEPS.length).replace(/_/g, "/")
46+
: source;
47+
48+
const SHOWCASE_DIST = "showcase/dist";
49+
source = source.includes(SHOWCASE_DIST)
50+
? "@showcase" +
51+
source.substring(source.indexOf(SHOWCASE_DIST) + SHOWCASE_DIST.length)
52+
: source;
53+
54+
// match to <source-file>?<timestamp-or-version>:<line>:<column>
55+
const [, sourceFile, lineN, columnN] = source.match(
56+
/^(.+)?(?:t=\d+|v=\w+):(\d+):(\d+)$/
57+
) ?? [, source, "", ""];
58+
59+
return {
60+
line: parseInt(lineN),
61+
column: parseInt(columnN),
62+
name: name,
63+
source: sourceFile,
64+
} satisfies ErrorStack;
65+
})
66+
.filter((e): e is ErrorStack => !!e);
67+
68+
return {
69+
message: error.message,
70+
stack,
71+
};
72+
}

js/showcase/src/vue/index.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
ConcreteComponent,
3+
createApp,
4+
DefineComponent,
5+
defineComponent,
6+
VNode,
7+
h as vueH,
8+
} from "vue";
9+
import { createShowcase, registerCase, ShowcaseConfigDef } from "..";
10+
import { createErrorBoundary } from "./ErrorBoundary";
11+
import {
12+
attach,
13+
instiate,
14+
render,
15+
renderCaseSplitted,
16+
renderNode,
17+
} from "./render";
18+
19+
export function setupShowcase(
20+
config: ShowcaseConfigDef<(props: any) => VNode> & {
21+
showcases: Record<string, DefineComponent>;
22+
}
23+
) {
24+
const virtualElem = document.createElement("div");
25+
Object.values(config.showcases).forEach(comp => {
26+
createApp(comp).mount(virtualElem);
27+
});
28+
29+
const icons = Object.fromEntries(
30+
Object.entries(config?.icons ?? {}).map(([iconName, iconComp]) => [
31+
iconName,
32+
vueH(iconComp),
33+
])
34+
);
35+
36+
createShowcase<ConcreteComponent<any>, VNode>({
37+
...config,
38+
icons,
39+
instiate,
40+
render,
41+
renderNode,
42+
renderCaseSplitted,
43+
attach,
44+
createErrorBoundary,
45+
});
46+
}
47+
48+
export default defineComponent({
49+
name: "Showcase",
50+
props: ["name", "props", "slots", "component"],
51+
setup(props, { slots }) {
52+
if (slots.default) {
53+
registerCase(props.name, () => slots.default!());
54+
} else {
55+
registerCase(props.name, {
56+
props: props.props,
57+
slots: props.slots,
58+
component: props.component,
59+
});
60+
}
61+
return () => vueH("div");
62+
},
63+
});

js/showcase/src/vue/render.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {
2+
createApp,
3+
customRef,
4+
defineComponent,
5+
Fragment,
6+
h as vueH,
7+
Ref,
8+
VNode,
9+
} from "vue";
10+
import { createEffect, MiniUI, renderH } from "../miniui";
11+
12+
export function instiate<P extends {}>(node: (p: P) => VNode, props: P): VNode {
13+
return vueH(node, props);
14+
}
15+
16+
export function render(node: () => VNode): globalThis.Node {
17+
const root = document.createElement("div");
18+
root.style.display = "contents";
19+
20+
createApp(node).mount(root);
21+
22+
return root;
23+
}
24+
25+
export function renderNode(node: VNode): globalThis.Node {
26+
return render(() => node);
27+
}
28+
29+
const ExternalAttach = defineComponent({
30+
name: "ExternalAttach",
31+
props: ["node"],
32+
setup({ node }) {
33+
return () =>
34+
vueH("div", {
35+
style: { display: "contents" },
36+
ref: elem => elem instanceof Element && renderH(elem, node),
37+
});
38+
},
39+
});
40+
41+
export function attach(node: MiniUI.Node): VNode {
42+
return vueH(ExternalAttach, { node });
43+
}
44+
45+
export const renderCaseSplitted = defineComponent<{
46+
inputs: VNode;
47+
component: (p: unknown) => VNode;
48+
props: Record<string, MiniUI.WritableSignal<unknown>>;
49+
slots: Record<string, MiniUI.Signal<VNode>>;
50+
events: Record<string, MiniUI.Signal<void>>;
51+
}>({
52+
name: "Case",
53+
props: ["inputs", "component", "props", "slots", "events"],
54+
setup({ inputs, component, props, slots, events }, ctx) {
55+
ctx.expose();
56+
const outProps: Record<
57+
string,
58+
Ref<unknown, unknown> | MiniUI.Signal<void>
59+
> = {};
60+
61+
for (const [propName, propValue] of Object.entries(props)) {
62+
outProps[propName] = customRef((track, trigger) => {
63+
createEffect(() => {
64+
propValue();
65+
trigger();
66+
});
67+
68+
return {
69+
get: () => (track(), propValue()),
70+
set: v => propValue(v),
71+
};
72+
});
73+
}
74+
75+
for (const [eventName, eventValue] of Object.entries(events)) {
76+
outProps[eventName] = eventValue;
77+
}
78+
79+
return () =>
80+
vueH(Fragment, null, [
81+
inputs,
82+
vueH(
83+
component,
84+
outProps,
85+
Object.fromEntries(
86+
Object.entries(slots).map(([slotName, slotSignal]) => [
87+
slotName,
88+
() => slotSignal(),
89+
])
90+
)
91+
),
92+
]);
93+
},
94+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script setup lang="ts">
2+
import Showcase from "@rustlanges/showcase/vue";
3+
import RsButton from "./rs-button.vue";
4+
5+
const props = {
6+
variant: {
7+
kind: "string",
8+
default: "primary",
9+
options: ["primary", "secondary", "text", "icon"],
10+
},
11+
};
12+
const slots = {
13+
default: {
14+
kind: "string",
15+
displayName: "label",
16+
default: "Button",
17+
},
18+
icon: "icon",
19+
};
20+
</script>
21+
22+
<template>
23+
<Showcase
24+
name="ButtonDef"
25+
:props="props"
26+
:slots="slots"
27+
:component="RsButton"
28+
/>
29+
30+
<Showcase name="Button">
31+
<RsButton> Hi </RsButton>
32+
</Showcase>
33+
</template>

js/vue/lib/components/rs-button.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script setup lang="ts">
2+
const variants = {
3+
primary: "rustlanges-button--primary",
4+
secondary: "rustlanges-button--secondary",
5+
text: "rustlanges-button--text",
6+
icon: "rustlanges-button--icon",
7+
};
8+
9+
type ButtonVariants = keyof typeof variants;
10+
11+
const { variant } = defineProps<{
12+
variant?: ButtonVariants;
13+
}>();
14+
</script>
15+
16+
<template>
17+
<button
18+
:class="[
19+
variants[variant ?? 'primary'] ?? variants.primary,
20+
'text-button rustlanges-button',
21+
]"
22+
v-bind="$attrs"
23+
>
24+
<slot v-if="variant !== 'icon'" />
25+
<slot name="icon" />
26+
</button>
27+
</template>

js/vue/lib/icons/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// export { Alert } from "./alert";
2+
// export { ArrowDown } from "./arrow-down";
3+
// export { ArrowLeft } from "./arrow-left";
4+
// export { ArrowRight } from "./arrow-right";
5+
// export { ArrowUp } from "./arrow-up";
6+
// export { Book } from "./book";
7+
// export { Close } from "./close";
8+
// export { Discord } from "./discord";
9+
// export { File } from "./file";
10+
// export { Github } from "./github";
11+
// export { Link } from "./link";
12+
// export { Linkedin } from "./linkedin";
13+
export { default as Location } from "./location.vue";
14+
// export { Menu } from "./menu";
15+
// export { Moon } from "./moon";
16+
// export { Project } from "./project";
17+
// export { Roadmap } from "./roadmap";
18+
// export { Share } from "./share";
19+
// export { StarBold } from "./star-bold";
20+
// export { SunLine } from "./sun-line";
21+
// export { Telegram } from "./telegram";
22+
// export { Twitter } from "./twitter";
23+
// export { Youtube } from "./youtube";

js/vue/lib/icons/location.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<svg
3+
width="1em"
4+
height="1em"
5+
viewBox="0 0 24 24"
6+
fill="none"
7+
xmlns="http://www.w3.org/2000/svg"
8+
v-bind="$attrs"
9+
>
10+
<path
11+
d="M12 11.5C11.337 11.5 10.7011 11.2366 10.2322 10.7678C9.76339 10.2989 9.5 9.66304 9.5 9C9.5 8.33696 9.76339 7.70107 10.2322 7.23223C10.7011 6.76339 11.337 6.5 12 6.5C12.663 6.5 13.2989 6.76339 13.7678 7.23223C14.2366 7.70107 14.5 8.33696 14.5 9C14.5 9.3283 14.4353 9.65339 14.3097 9.95671C14.1841 10.26 13.9999 10.5356 13.7678 10.7678C13.5356 10.9999 13.26 11.1841 12.9567 11.3097C12.6534 11.4353 12.3283 11.5 12 11.5ZM12 2C10.1435 2 8.36301 2.7375 7.05025 4.05025C5.7375 5.36301 5 7.14348 5 9C5 14.25 12 22 12 22C12 22 19 14.25 19 9C19 7.14348 18.2625 5.36301 16.9497 4.05025C15.637 2.7375 13.8565 2 12 2Z"
12+
fill="currentColor"
13+
/>
14+
</svg>
15+
</template>

js/vue/lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./components/rs-button.showcase.vue";
2+
// export * from "./icons";

js/vue/lib/showcases.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// import "./components/avatar/avatar.showcase";
2+
// import "./components/badge/badge.showcase";
3+
export { default as ButtonShowcase } from "./components/rs-button.showcase.vue";
4+
// import "./components/calendar/calendar.showcase";
5+
// import "./components/card/card.showcase";
6+
// import "./components/chip/chip.showcase";
7+
// import "./components/collaborators/collaborators.showcase";
8+
// import "./components/contact-form/contact-form.showcase";
9+
// import "./components/dropdown/dropdown.showcase";
10+
// import "./components/dropdown-tree/dropdown-tree.showcase";
11+
// import "./components/flap/flap.showcase";
12+
// import "./components/input/input.showcase";
13+
// import "./components/input-search/input-search.showcase";
14+
// import "./components/level/level.showcase";
15+
// import "./components/progress-bar/progress-bar.showcase";
16+
// import "./components/radio/radio.showcase";
17+
// import "./components/tag/tag.showcase";

js/vue/showcase/main.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { setupShowcase } from "@rustlanges/showcase/vue";
2+
import * as icons from "../lib/icons";
3+
4+
import "@rustlanges/showcase/styles.css";
5+
import "./styles.css";
6+
7+
import * as showcases from "../lib/showcases";
8+
9+
setupShowcase({
10+
showcases,
11+
icons,
12+
});

0 commit comments

Comments
 (0)