@typed-grid-areas/vue 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ import { type Ref, type VNode } from "vue";
2
+ /**
3
+ * Renders your own element in place of the adapter's `div`, Reka UI's `Slot`
4
+ * algorithm: the first child that isn't a comment, cloned with `attrs` merged
5
+ * into its props (its own props win, classes and styles combine, handlers
6
+ * chain) and `forced` on top, its own ref kept and `ref` (when given) added.
7
+ */
8
+ export declare function renderAsChild(children: ReadonlyArray<VNode> | undefined, attrs: Record<string, unknown>, forced: Record<string, unknown>, ref?: Ref<Element | null>): Array<VNode> | VNode | null;
@@ -0,0 +1,36 @@
1
+ import { cloneVNode, Comment, Fragment, mergeProps } from "vue";
2
+ /** Slot content with its fragments flattened (`v-for`, `<template>`), as Reka UI's `Slot` reads it. */
3
+ function flatten(children) {
4
+ return children.flatMap(child => child.type === Fragment && Array.isArray(child.children)
5
+ ? flatten(child.children)
6
+ : [child]);
7
+ }
8
+ /**
9
+ * Renders your own element in place of the adapter's `div`, Reka UI's `Slot`
10
+ * algorithm: the first child that isn't a comment, cloned with `attrs` merged
11
+ * into its props (its own props win, classes and styles combine, handlers
12
+ * chain) and `forced` on top, its own ref kept and `ref` (when given) added.
13
+ */
14
+ export function renderAsChild(children, attrs, forced, ref) {
15
+ if (children === undefined) {
16
+ return null;
17
+ }
18
+ const flat = flatten(children);
19
+ const index = flat.findIndex(child => child.type !== Comment);
20
+ const child = flat[index];
21
+ if (child === undefined) {
22
+ return flat;
23
+ }
24
+ // The child's own ref lives on the vnode, which the clone keeps.
25
+ const { ref: _ref, ref_key: _key, ref_for: _for, ...own } = child.props ?? {};
26
+ const props = mergeProps(attrs, own, forced);
27
+ if (ref !== undefined) {
28
+ props.ref = ref;
29
+ }
30
+ const cloned = cloneVNode({ ...child, props: {} }, props, true);
31
+ if (flat.length === 1) {
32
+ return cloned;
33
+ }
34
+ flat[index] = cloned;
35
+ return flat;
36
+ }
@@ -0,0 +1,67 @@
1
+ import { type DefineSetupFnComponent } from "vue";
2
+ import type { GridConfig } from "typed-grid-areas";
3
+ /** Props of a slot component (`<Card.icon>`) and of a grid component (`<Card>`). */
4
+ export type SlotComponentProps = {
5
+ /**
6
+ * Render your own single child element as the slot (or grid) instead of a
7
+ * `div`, with the attributes merged onto it (`as-child` in templates).
8
+ */
9
+ asChild?: boolean;
10
+ };
11
+ /** The props of a grid component (`<Card>`). */
12
+ export type GridComponentProps = SlotComponentProps;
13
+ /** A slot component (`<Card.icon>`): renders the element that fills one area. */
14
+ export type SlotComponent = DefineSetupFnComponent<SlotComponentProps>;
15
+ /**
16
+ * The component a definition renders as: the grid itself, with a member
17
+ * component per area, named exactly like the area. An area nested through
18
+ * `nest` is the nested grid's component instead, with that grid's members
19
+ * (`<Card.content.icon>`).
20
+ */
21
+ export type GridComponent<TAreaName extends string, TNest = {}> = DefineSetupFnComponent<GridComponentProps> & {
22
+ readonly [K in TAreaName]: K extends keyof TNest ? TNest[K] : SlotComponent;
23
+ };
24
+ /** Any grid component, whatever its areas. */
25
+ type AnyGridComponent = GridComponent<any, any>;
26
+ /** Options of `createGridComponent`. */
27
+ export type GridComponentOptions<TAreaName extends string> = {
28
+ /**
29
+ * Grids nested into areas of this one: `nest: { content: Content }` makes
30
+ * `<Card.content>` both the `content` slot and the grid of `Content`.
31
+ */
32
+ nest?: {
33
+ readonly [K in TAreaName]?: AnyGridComponent;
34
+ };
35
+ };
36
+ /**
37
+ * Names Vue reads on a component, so an area member named like one would
38
+ * change how the component behaves.
39
+ */
40
+ declare const RESERVED_AREA_NAMES: readonly ["length", "props", "emits", "slots", "inheritAttrs", "displayName", "compatConfig"];
41
+ /** An area name Vue reserves on a component: one of `RESERVED_AREA_NAMES`, or any starting with `__`. */
42
+ export type ReservedAreaName = (typeof RESERVED_AREA_NAMES)[number] | `__${string}`;
43
+ /**
44
+ * Rejects a definition with a reserved area name at compile time: the
45
+ * definition must then have a property it can't have, named after the problem.
46
+ */
47
+ type WithoutReservedAreas<TAreaName extends string> = [
48
+ Extract<TAreaName, ReservedAreaName>
49
+ ] extends [never] ? unknown : {
50
+ [K in `area "${Extract<TAreaName, ReservedAreaName>}" is reserved by Vue; rename it`]: never;
51
+ };
52
+ /**
53
+ * Turns a definition into the component it renders as.
54
+ *
55
+ * `<Card>` renders the grid (inside its own size container when the
56
+ * definition has breakpoints) and puts the definition's CSS on the page;
57
+ * `<Card.icon>` renders the slot that fills the `icon` area. Both take
58
+ * `class`, `style`, any other attribute, and `as-child` to make your own
59
+ * element the slot (or grid).
60
+ *
61
+ * Bind it in `<script setup>` (or return it from `setup()`), so templates can
62
+ * use its members as tags. Create it once per definition.
63
+ */
64
+ export declare function createGridComponent<TAreaName extends string, const TNest extends GridComponentOptions<TAreaName>["nest"] = {}>(definition: GridConfig<TAreaName> & WithoutReservedAreas<TAreaName>, options?: {
65
+ nest?: TNest;
66
+ }): GridComponent<TAreaName, TNest>;
67
+ export {};
@@ -0,0 +1,123 @@
1
+ import { defineComponent, h, onMounted, onUpdated, ref, } from "vue";
2
+ import { createSlotCheck, gridMarkup, isDevelopment } from "typed-grid-areas/adapter";
3
+ import { renderAsChild } from "./asChild.js";
4
+ import { useGridStyle } from "./gridStyles.js";
5
+ /**
6
+ * Names Vue reads on a component, so an area member named like one would
7
+ * change how the component behaves.
8
+ */
9
+ const RESERVED_AREA_NAMES = [
10
+ "length",
11
+ "props",
12
+ "emits",
13
+ "slots",
14
+ "inheritAttrs",
15
+ "displayName",
16
+ "compatConfig",
17
+ ];
18
+ function isReserved(area) {
19
+ return area.startsWith("__") || RESERVED_AREA_NAMES.includes(area);
20
+ }
21
+ /** The stateful component behind a grid component: renders the grid and puts its CSS on the page. */
22
+ function createGrid(definition) {
23
+ const markup = gridMarkup(definition);
24
+ return defineComponent({
25
+ name: "Grid",
26
+ inheritAttrs: false,
27
+ props: {
28
+ asChild: Boolean,
29
+ /** The area of an outer grid this grid fills, when it is nested. */
30
+ dataArea: String,
31
+ },
32
+ setup(props, { attrs, slots }) {
33
+ useGridStyle(definition);
34
+ // In development, check the slots after mounting and after every update.
35
+ const gridRef = ref(null);
36
+ if (isDevelopment()) {
37
+ const checkSlots = createSlotCheck(definition, {
38
+ strayChildRemedy: "make it a slot with as-child",
39
+ });
40
+ const check = () => {
41
+ if (gridRef.value !== null) {
42
+ for (const warning of checkSlots(gridRef.value)) {
43
+ // eslint-disable-next-line no-console
44
+ console.warn(warning);
45
+ }
46
+ }
47
+ };
48
+ onMounted(check);
49
+ onUpdated(check);
50
+ }
51
+ return () => {
52
+ const { sizeContainer, grid } = markup.elements(props.dataArea);
53
+ const gridElement = props.asChild
54
+ ? renderAsChild(slots.default?.(), attrs, grid.attributes, gridRef)
55
+ : h("div", { ...attrs, ...grid.attributes, ref: gridRef }, slots.default?.());
56
+ return sizeContainer === undefined
57
+ ? gridElement
58
+ : h("div", { ...sizeContainer.attributes, style: sizeContainer.style }, [gridElement]);
59
+ };
60
+ },
61
+ });
62
+ }
63
+ function createSlotComponent(area) {
64
+ return defineComponent({
65
+ name: `${area}Area`,
66
+ inheritAttrs: false,
67
+ props: { asChild: Boolean },
68
+ setup(props, { attrs, slots }) {
69
+ return () => props.asChild
70
+ ? renderAsChild(slots.default?.(), attrs, { "data-area": area })
71
+ : h("div", { ...attrs, "data-area": area }, slots.default?.());
72
+ },
73
+ });
74
+ }
75
+ /**
76
+ * A functional component rendering `component` with the attributes it is
77
+ * given (and `extra` on top), passing its slots through. Its own properties
78
+ * stay free for the area members: Vue reads none but the reserved ones.
79
+ */
80
+ function passThrough(component, extra = {}) {
81
+ const render = (_props, { attrs, slots }) => h(component, { ...attrs, ...extra }, slots);
82
+ render.inheritAttrs = false;
83
+ return render;
84
+ }
85
+ /** The members (one per area) a grid component carries. */
86
+ const MEMBERS = Symbol("typed-grid-areas members");
87
+ /** Defines `members` on a component; `defineProperty`, since a function's `name` is read-only. */
88
+ function withMembers(component, members) {
89
+ for (const [key, value] of Object.entries(members)) {
90
+ Object.defineProperty(component, key, { value, enumerable: true, configurable: true });
91
+ }
92
+ Object.defineProperty(component, MEMBERS, { value: members });
93
+ return component;
94
+ }
95
+ /**
96
+ * Turns a definition into the component it renders as.
97
+ *
98
+ * `<Card>` renders the grid (inside its own size container when the
99
+ * definition has breakpoints) and puts the definition's CSS on the page;
100
+ * `<Card.icon>` renders the slot that fills the `icon` area. Both take
101
+ * `class`, `style`, any other attribute, and `as-child` to make your own
102
+ * element the slot (or grid).
103
+ *
104
+ * Bind it in `<script setup>` (or return it from `setup()`), so templates can
105
+ * use its members as tags. Create it once per definition.
106
+ */
107
+ export function createGridComponent(definition, options = {}) {
108
+ const members = {};
109
+ for (const area of definition.areas) {
110
+ if (isReserved(area)) {
111
+ throw new Error(`typed-grid-areas: the area name "${area}" is reserved by Vue on a component, so it ` +
112
+ `can't be a member of the grid component. Rename the area.`);
113
+ }
114
+ }
115
+ for (const area of definition.areas) {
116
+ const nested = options.nest?.[area];
117
+ members[area] =
118
+ nested === undefined
119
+ ? createSlotComponent(area)
120
+ : withMembers(passThrough(nested, { "data-area": area }), nested[MEMBERS] ?? {});
121
+ }
122
+ return withMembers(passThrough(createGrid(definition)), members);
123
+ }
@@ -0,0 +1,20 @@
1
+ import type { GridConfig } from "typed-grid-areas";
2
+ /**
3
+ * Puts a definition's CSS on the page, from a component's `setup`: into the
4
+ * current request's SSR context when server rendering, into `<head>` in the
5
+ * browser.
6
+ */
7
+ export declare function useGridStyle(definition: GridConfig<string>): void;
8
+ /**
9
+ * The style tags of every definition rendered with an SSR context, one per
10
+ * definition, as HTML for the server-rendered `<head>`. In the browser each
11
+ * grid then finds its tag and adds no second one.
12
+ *
13
+ * @example
14
+ * ```js
15
+ * const ctx = {};
16
+ * const html = await renderToString(app, ctx);
17
+ * head += renderGridStyles(ctx);
18
+ * ```
19
+ */
20
+ export declare function renderGridStyles(ssrContext: object): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Putting definitions' CSS on the page (ADR 0005). Vue has no API for
3
+ * `<head>`, so in the browser each grid inserts its definition's tag itself,
4
+ * and on the server it registers its definition in the request's SSR
5
+ * context, for the app's server entry to render with `renderGridStyles`.
6
+ */
7
+ import { inject, ssrContextKey } from "vue";
8
+ import { gridStyleTags, insertGridStyle } from "typed-grid-areas/adapter";
9
+ /**
10
+ * Where a request's definitions are kept on its SSR context. `Symbol.for`, so
11
+ * a second copy of this package (one bundled into the app, one external)
12
+ * still finds them.
13
+ */
14
+ const DEFINITIONS = Symbol.for("typed-grid-areas/vue definitions");
15
+ /**
16
+ * Puts a definition's CSS on the page, from a component's `setup`: into the
17
+ * current request's SSR context when server rendering, into `<head>` in the
18
+ * browser.
19
+ */
20
+ export function useGridStyle(definition) {
21
+ // `inject` with a default, since the context is only provided on the server.
22
+ const ssrContext = inject(ssrContextKey, null);
23
+ if (ssrContext !== null) {
24
+ (ssrContext[DEFINITIONS] ??= new Set()).add(definition);
25
+ }
26
+ else if (typeof document !== "undefined") {
27
+ insertGridStyle(document, definition);
28
+ }
29
+ }
30
+ /**
31
+ * The style tags of every definition rendered with an SSR context, one per
32
+ * definition, as HTML for the server-rendered `<head>`. In the browser each
33
+ * grid then finds its tag and adds no second one.
34
+ *
35
+ * @example
36
+ * ```js
37
+ * const ctx = {};
38
+ * const html = await renderToString(app, ctx);
39
+ * head += renderGridStyles(ctx);
40
+ * ```
41
+ */
42
+ export function renderGridStyles(ssrContext) {
43
+ return gridStyleTags(ssrContext[DEFINITIONS] ?? []);
44
+ }
@@ -0,0 +1,2 @@
1
+ export { createGridComponent, type GridComponent, type GridComponentOptions, type GridComponentProps, type SlotComponent, type SlotComponentProps, } from "./gridComponent.ts";
2
+ export { renderGridStyles } from "./gridStyles.ts";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createGridComponent, } from "./gridComponent.js";
2
+ export { renderGridStyles } from "./gridStyles.js";
package/package.json CHANGED
@@ -1 +1,52 @@
1
- {"name":"@typed-grid-areas/vue","version":"0.0.0"}
1
+ {
2
+ "name": "@typed-grid-areas/vue",
3
+ "version": "0.1.0",
4
+ "description": "Vue adapter for typed-grid-areas: render a grid definition as typed slot components.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/MichaelBuss/typed-grid-areas.git",
10
+ "directory": "packages/vue"
11
+ },
12
+ "homepage": "https://github.com/MichaelBuss/typed-grid-areas#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/MichaelBuss/typed-grid-areas/issues"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "prepack": "npm run build && cp ../../README.md ../../LICENSE .",
28
+ "postpack": "rm README.md LICENSE",
29
+ "test": "vitest run --project unit --project node",
30
+ "test:browser": "vitest run --project browser --project browser-development",
31
+ "typecheck": "node scripts/vue-tsc.cjs --noEmit"
32
+ },
33
+ "peerDependencies": {
34
+ "vue": "^3.5.0"
35
+ },
36
+ "dependencies": {
37
+ "typed-grid-areas": "^0.2.0"
38
+ },
39
+ "devDependencies": {
40
+ "@typed-grid-areas/acceptance": "*",
41
+ "@types/node": "^22.10.5",
42
+ "@vitejs/plugin-vue": "^6.0.10",
43
+ "@vitest/browser-playwright": "^5.0.1",
44
+ "happy-dom": "^20.14.5",
45
+ "playwright": "^1.64.1",
46
+ "typescript": "~6.0.3",
47
+ "vitest": "^5.0.1",
48
+ "vitest-browser-vue": "^3.1.0",
49
+ "vue": "^3.5.44",
50
+ "vue-tsc": "^3.3.13"
51
+ }
52
+ }