@koordinates/xstate-tree 1.0.0-beta.1

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.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # xstate-tree
2
+
3
+ xstate-tree is a React state management framework built using XState. It allows you to create a tree of XState machines and map them to a tree of React views representing them as a UI.
4
+
5
+ ## Overview
6
+
7
+ Each machine that forms the tree representing your UI has an associated set of selector, action, view functions, and "slots"
8
+ - Selector functions are provided with the current context of the machine, a function to determine if it can handle a given event and a function to determine if it is in a given state, and expose the returned result to the view.
9
+ - Action functions are provided with the `send` method bound to the machines interpreter and the result of calling the selector function
10
+ - Slots are how children of the machine are exposed to the view. They can be either single slot, a single machine, or multi slot for when you have a list of actors.
11
+ - View functions are React views provided with the output of the selector and action functions, a function to determine if the machine is in a given state, and the currently active slots
12
+
13
+ ## API
14
+
15
+ To assist in making xstate-tree easy to use with TypeScript there are "builder" functions for selectors, actions, views and the final XState tree machine itself. These functions primarily exist to type the arguments passed into the selector/action/view functions.
16
+
17
+ * `buildSelectors`, first argument is the machine we are creating selectors for, second argument is the selector factory which receives the machines context as the first argument. It also memoizes the selector factory for better rendering performance
18
+ * `buildActions`, first argument is the machine we are creating actions for, the second argument is the result of `buildSelectors` and the third argument is the actions factory which receives an XState `send` function and the result of calling the selectors factory. It also memoizes the selector factory for better rendering performance
19
+ * `buildView`, first argument is the machine we are creating a view for, second argument is the selector factory, third argument is the actions factory, fourth argument is the array of slots and the fifth argument is the view function itself which gets supplied the selectors, actions, slots and `inState` method as props. It wraps the view in a React.memo
20
+ * `buildXStateTreeMachine` takes the results of `buildSelectors`, `buildActions`, `buildView` and the list of slots and returns an xstate-tree compatible machine
21
+
22
+ ### Slots
23
+
24
+ Slots are how invoked/spawned children of the machine are supplied to the Machines view. The child machines get wrapped into a React component responsible for rendering the machine itself. Since the view is provided with these components it is responsible for determining where in the view output they show up. This leaves the view in complete control of where the child views are placed.
25
+
26
+ Slotted machines are determined based on the id of the invoked/spawned machine. There are two types of slots, single and multi. Single slots are for invoked machines, where there will only be a single machine per slot. Multi slots are for spawned machines where there are multiple children per slot, rendered as a group; think lists. There is a set of helper functions for creating slots which in turn can be used to get the id for the slot.
27
+
28
+ `singleSlot` accepts the name of the slot (must not end in `s`) as the first argument and returns an object with a method `getId()` that returns the id of the slot.
29
+ `multiSlot` accepts the name of the slot (must end in `s`) and returns an object with a method `getId(id: string)` that returns the id of the slot
30
+
31
+ You should always use the `getId` methods when invoking/spawning something into a slot. Each slot the machine has must be represented by a call to `singleSlot` or `multiSlot` and stored into an array of slots. These slots must be passed to the `buildView` and `buildXStateTreeMachine` functions.
32
+
33
+ ### Inter-machine communication
34
+
35
+ Communicating between multiple independent xstate machines is done via the `broadcast` function.
36
+ Any event broadcast via this function is sent to every machine that has the event in its `nextEvents` array, so it won't get sent to machines that have no handler for the event.
37
+
38
+ To get access to the type information for these events in a machine listening for it, use the `PickEvent` type to extract the events you are interested in
39
+
40
+ ie `PickEvent<"FOO" | "BAR">` will return `{type: "FOO" } | { type: "BAR" }` which can be added to your machines event union.
41
+
42
+ To provide the type information on what events are available you add them to the global XstateTreeEvents interface. This is done using `declare global`
43
+
44
+ ```
45
+ declare global {
46
+ interface XstateTreeEvents {
47
+ BASIC: string;
48
+ WITH_PAYLOAD: { a: "payload" }
49
+ }
50
+ }
51
+ ```
52
+
53
+ That adds two events to the system, a no payload event (`{ type: "BASIC" }`) and event with payload (`{ type: "WITH_PAYLOAD"; a: "payload" }`). These events will now be visible in the typings for `broadcast` and `PickEvent`. The property name is the `type` of the event and the type of the property is the payload of the event. If the event has no payload, use `string`.
54
+
55
+ These events can be added anywhere, either next to a component for component specific events or in a module for events that are for multiple machines. One thing that it is important to keep in mind is that these `declare global` declarations must be loaded by the `.d.ts` files when importing the component, otherwise the events will be missing. Which means
56
+
57
+ 1. If they are in their own file, say for a module level declaration, that file will need to be imported somewhere. Somewhere that using a component will trigger the import
58
+ 2. If they are tied to a component they need to be in the index.ts file that imports the view/selectors/actions etc and calls `buildXstateTreeMachine`. If they are in the file containing those functions the index.d.ts file will not end up importing them.
59
+
60
+
61
+ ### Storybook
62
+
63
+ It's relatively uncomplicated to display xstate-tree views directly in Storybook. Since the views are plain React components
64
+ that accept selectors/actions/slots/inState as props you can just import the view and render it in a Story
65
+
66
+ There are a few utilities in xstate-tree to make this easier
67
+
68
+ #### `buildViewProps`
69
+ This is a builder function that accepts a view to provide typings and then an object containing
70
+ actions/selector fields. With the typings it provides these fields are type safe and you can autocomplete them.
71
+
72
+ It returns the props object and extends it with an `inState` factory function, so you can destructure it for use in Stories. The `inState` function accepts a state string as an argument, and returns a function that returns true if the state supplied matches that. So you can easily render the view in a specific machine state in the Story
73
+ ```
74
+ const { actions, selectors, inState } = buildViewProps(view, { actions: {], selectors: {} });
75
+ ```
76
+
77
+ #### `genericSlotsTestingDummy`
78
+
79
+ This is a simple Proxy object that renders a <div> containing the name of the slot whenever rendering
80
+ a slot is attempted in the view. This will suffice as an argument for the slots prop in most views
81
+ when rendering them in a Story
82
+
83
+ #### `slotTestingDummyFactory`
84
+
85
+ This is not relevant if using the render-view-component approach. But useful if you
86
+ are planning on rendering the view using the xstate-tree machine itself, or testing the machine
87
+ via the view.
88
+
89
+ It's a simple function that takes a name argument and returns a basic xstate-tree machine that you
90
+ can replace slot services with. It just renders a div containing the name supplied
@@ -0,0 +1,62 @@
1
+ import React from "react";
2
+ /**
3
+ * @public
4
+ */
5
+ export function buildSelectors(__machine, selectors) {
6
+ let lastState = undefined;
7
+ let lastCachedResult = undefined;
8
+ let lastCtxRef = undefined;
9
+ return (ctx, canHandleEvent, inState, currentState) => {
10
+ // Handles caching to ensure stable references to selector results
11
+ // Only re-run the selector if
12
+ // * The reference to the context object has changed (the context object should never be mutated)
13
+ // * The last state we ran the selectors in has changed. This is to ensure `canHandleEvent` and `inState` calls aren't stale
14
+ if (lastCtxRef === ctx &&
15
+ lastState === currentState &&
16
+ lastCachedResult !== undefined) {
17
+ return lastCachedResult;
18
+ }
19
+ else {
20
+ const result = selectors(ctx, canHandleEvent, inState, currentState);
21
+ lastCtxRef = ctx;
22
+ lastCachedResult = result;
23
+ lastState = currentState;
24
+ return result;
25
+ }
26
+ };
27
+ }
28
+ /**
29
+ * @public
30
+ */
31
+ export function buildActions(__machine, __selectors, actions) {
32
+ let lastSelectorResult = undefined;
33
+ let lastCachedResult = undefined;
34
+ let lastSendReference = undefined;
35
+ return (send, selectors) => {
36
+ if (lastSelectorResult === selectors &&
37
+ lastCachedResult !== undefined &&
38
+ lastSendReference === send) {
39
+ return lastCachedResult;
40
+ }
41
+ lastCachedResult = actions(send, selectors);
42
+ lastSelectorResult = selectors;
43
+ lastSendReference = send;
44
+ return lastCachedResult;
45
+ };
46
+ }
47
+ /**
48
+ * @public
49
+ */
50
+ export function buildView(__machine, __selectors, __actions, __slots, view) {
51
+ return React.memo(view);
52
+ }
53
+ /**
54
+ * @public
55
+ */
56
+ export function buildXStateTreeMachine(machine, meta) {
57
+ const copiedMeta = { ...meta };
58
+ copiedMeta.xstateTreeMachine = true;
59
+ machine.config.meta = { ...machine.config.meta, ...copiedMeta };
60
+ machine.meta = { ...machine.meta, ...copiedMeta };
61
+ return machine;
62
+ }
package/lib/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./builders";
2
+ export * from "./slots";
3
+ export { broadcast, buildRootComponent, onBroadcast } from "./xstateTree";
4
+ export * from "./types";
5
+ export { buildStorybookComponent, buildTestRootComponent, buildViewProps, genericSlotsTestingDummy, slotTestingDummyFactory, } from "./testingUtilities";
6
+ export { Link, buildCreateRoute, matchRoute, } from "./routing";
7
+ export { loggingMetaOptions } from "./useService";
8
+ export { lazy } from "./lazy";
package/lib/lazy.js ADDED
@@ -0,0 +1,51 @@
1
+ import { identity } from "lodash";
2
+ import React from "react";
3
+ import { createMachine, } from "xstate";
4
+ import { buildActions, buildSelectors, buildView, buildXStateTreeMachine, } from "./builders";
5
+ import { singleSlot } from "./slots";
6
+ /**
7
+ * @public
8
+ * Wraps an xstate-tree returning Promise (generated by `import()` in an xstate-tree machine responsible for
9
+ * booting up the machine upon resolution
10
+ *
11
+ * @param factory - the factory function that returns the promise that resolves to the machine
12
+ * @param options - configure loading component and context to invoke machine with
13
+ * @returns an xstate-tree machine that wraps the promise, invoking the resulting machine when it resolves
14
+ */
15
+ export function lazy(factory, { Loader = () => null, withContext = () => ({}), } = {}) {
16
+ const loadedMachineSlot = singleSlot("loadedMachine");
17
+ const slots = [loadedMachineSlot];
18
+ const machine = createMachine({
19
+ initial: "loading",
20
+ states: {
21
+ loading: {
22
+ invoke: {
23
+ src: () => factory,
24
+ onDone: "rendering",
25
+ },
26
+ },
27
+ rendering: {
28
+ invoke: {
29
+ id: loadedMachineSlot.getId(),
30
+ src: (_ctx, e) => {
31
+ return e.data.withContext({ ...e.data.context, ...withContext() });
32
+ },
33
+ },
34
+ },
35
+ },
36
+ });
37
+ const selectors = buildSelectors(machine, identity);
38
+ const actions = buildActions(machine, selectors, identity);
39
+ const view = buildView(machine, selectors, actions, slots, ({ slots, inState }) => {
40
+ if (inState("loading")) {
41
+ return React.createElement(Loader, null);
42
+ }
43
+ return React.createElement(slots.loadedMachine, null);
44
+ });
45
+ return buildXStateTreeMachine(machine, {
46
+ actions,
47
+ selectors,
48
+ slots,
49
+ view,
50
+ });
51
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { useHref } from "./useHref";
3
+ /**
4
+ * @public
5
+ * Renders an anchor tag pointing at the provided Route
6
+ *
7
+ * The query/params/meta props are conditionally required based on the
8
+ * route passed as the To parameter
9
+ */
10
+ export function Link({ to, children, testId, ...rest }) {
11
+ // @ts-ignore, these fields _might_ exist, so typechecking doesn't believe they exist
12
+ // and everything that consumes params/query already checks for undefined
13
+ const { params, query, meta, ...props } = rest;
14
+ const href = useHref(to, params, query);
15
+ return (React.createElement("a", { ...props, href: href, "data-testid": testId, onClick: (e) => {
16
+ if (props.onClick?.(e) === false) {
17
+ return;
18
+ }
19
+ // Holding the Command key on Mac or the Control Key on Windows while clicking the link will open a new tab/window
20
+ // TODO: add global callback to prevent this
21
+ if (e.metaKey || e.ctrlKey) {
22
+ return;
23
+ }
24
+ e.preventDefault();
25
+ to.navigate({ params, query, meta });
26
+ } }, children));
27
+ }
@@ -0,0 +1,185 @@
1
+ import { isNil } from "lodash";
2
+ import { match, compile } from "path-to-regexp";
3
+ import { parse, stringify } from "query-string";
4
+ import { joinRoutes } from "../joinRoutes";
5
+ /**
6
+ * @public
7
+ */
8
+ export function buildCreateRoute(history, basePath) {
9
+ function navigate({ history, url, meta, }) {
10
+ const method = meta?.replace ? history.replace : history.push;
11
+ method(url, {
12
+ meta,
13
+ previousUrl: window.location.pathname,
14
+ });
15
+ }
16
+ return {
17
+ /**
18
+ * Creates a dynamic Route using the supplied options
19
+ *
20
+ * The return value of dynamicRoute is a function that accepts the routes "dynamic" options
21
+ * The argument to dynamicRoute itself is the params/query/meta schemas defining the route
22
+ *
23
+ * The returned function accepts a singular option object with the following fields
24
+ *
25
+ * `event`, the string constant for the routes event
26
+ * `matches`, a function that is passed a url/query string and determines if the route matches
27
+ * if the route is matched it returns the extracted params/query objects
28
+ * `reverse`, a function that is passed params/query objects and turns them into a URL
29
+ *
30
+ * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
31
+ */
32
+ dynamicRoute: function createDynamicRoute(opts) {
33
+ return ({ event, matches, reverse, }) => {
34
+ return {
35
+ paramsSchema: opts?.params,
36
+ querySchema: opts?.query,
37
+ event,
38
+ history,
39
+ basePath,
40
+ parent: undefined,
41
+ // @ts-ignore the usual
42
+ getEvent({ params, query, meta } = {}) {
43
+ return { type: event, params, query, meta };
44
+ },
45
+ // @ts-ignore not sure how to type this
46
+ matches(url, search) {
47
+ const query = parse(search);
48
+ const match = matches(url, query);
49
+ if (match === false) {
50
+ return undefined;
51
+ }
52
+ if (opts?.params && "params" in match) {
53
+ opts.params.parse(match.params);
54
+ }
55
+ if (opts?.query && "query" in match) {
56
+ opts.query.parse(match.query);
57
+ }
58
+ return { type: event, originalUrl: `${url}${search}`, ...match };
59
+ },
60
+ // @ts-ignore not sure how to type this correctly
61
+ // The types from external to this function are correct however
62
+ reverse({ params, query } = {}) {
63
+ return reverse({ params, query });
64
+ },
65
+ // @ts-ignore not sure how to type this correctly
66
+ // The types from external to this function are correct however
67
+ navigate({ params, query, meta } = {}) {
68
+ // @ts-ignore same problem
69
+ const url = this.reverse({ params, query });
70
+ navigate({
71
+ url: joinRoutes(this.basePath, url),
72
+ meta,
73
+ history: this.history,
74
+ });
75
+ },
76
+ };
77
+ };
78
+ },
79
+ /**
80
+ * Creates a static Route using the supplied options
81
+ *
82
+ * The return value of staticRoute is a function that accepts the routes options
83
+ * The only argument to staticRoute itself is an optional parent route
84
+ *
85
+ * The returned function accepts 3 arguments
86
+ *
87
+ * 1. URL of the route
88
+ * 2. The event type of the route
89
+ * 3. The routes options, params schema, query schema and meta type
90
+ *
91
+ * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
92
+ *
93
+ * When creating a route that has a parent route, the following happens
94
+ *
95
+ * 1. The parent routes url is prepended to the routes URL
96
+ * 2. The parents params schema is merged with the routes schema
97
+ * 3. The parents meta type is merged with the routes meta type
98
+ */
99
+ staticRoute: function createStaticRoute(baseRoute) {
100
+ return (url, event, opts) => {
101
+ if (baseRoute && isNil(baseRoute.url)) {
102
+ throw new Error("Somehow constructing a route with a base route missing a URL, did you pass a dynamic route?");
103
+ }
104
+ const urlWithTrailingSlash = url.endsWith("/") ? url : `${url}/`;
105
+ const fullUrl = baseRoute
106
+ ? joinRoutes(baseRoute.url, urlWithTrailingSlash)
107
+ : urlWithTrailingSlash;
108
+ const matcher = match(fullUrl, {});
109
+ const reverser = compile(fullUrl);
110
+ const paramsSchema = baseRoute?.paramsSchema
111
+ ? opts?.params
112
+ ? baseRoute.paramsSchema.merge(opts.params)
113
+ : baseRoute.paramsSchema
114
+ : opts?.params
115
+ ? opts.params
116
+ : undefined;
117
+ return {
118
+ paramsSchema,
119
+ querySchema: opts?.query,
120
+ event,
121
+ history,
122
+ basePath,
123
+ url: fullUrl,
124
+ parent: baseRoute,
125
+ // @ts-ignore the usual
126
+ getEvent({ params, query, meta } = {}) {
127
+ return { type: event, params, query, meta };
128
+ },
129
+ // @ts-ignore not sure how to type this
130
+ matches(url, search) {
131
+ const fullUrl = url.endsWith("/") ? url : `${url}/`;
132
+ const matches = matcher(fullUrl);
133
+ if (matches === false) {
134
+ return undefined;
135
+ }
136
+ const params = matches.params;
137
+ if (params && paramsSchema) {
138
+ paramsSchema.parse(params);
139
+ }
140
+ const query = parse(search);
141
+ if (opts?.query) {
142
+ opts.query.parse(query);
143
+ }
144
+ return {
145
+ type: event,
146
+ originalUrl: `${fullUrl}${search}`,
147
+ params,
148
+ query,
149
+ };
150
+ },
151
+ // @ts-ignore not sure how to type this correctly
152
+ // The types from external to this function are correct however
153
+ reverse({ params, query } = {}) {
154
+ const url = (() => {
155
+ if (params) {
156
+ // @ts-ignore same problem
157
+ return reverser(params);
158
+ }
159
+ else {
160
+ return reverser();
161
+ }
162
+ })();
163
+ if (!isNil(query)) {
164
+ return `${url}?${stringify(query)}`;
165
+ }
166
+ else {
167
+ return url;
168
+ }
169
+ },
170
+ // @ts-ignore not sure how to type this correctly
171
+ // The types from external to this function are correct however
172
+ navigate({ params, query, meta } = {}) {
173
+ // @ts-ignore same problem
174
+ const url = this.reverse({ params, query });
175
+ navigate({
176
+ url: joinRoutes(this.basePath, url),
177
+ meta,
178
+ history: this.history,
179
+ });
180
+ },
181
+ };
182
+ };
183
+ },
184
+ };
185
+ }
@@ -0,0 +1 @@
1
+ export { buildCreateRoute, } from "./createRoute";
@@ -0,0 +1,47 @@
1
+ import { broadcast } from "../../xstateTree";
2
+ import { matchRoute } from "../matchRoute";
3
+ /**
4
+ * @internal
5
+ */
6
+ export function handleLocationChange(routes, basePath, path, search, setActiveRouteEvents, meta) {
7
+ console.debug("[xstate-tree] Matching routes", basePath, path, search, meta);
8
+ const match = matchRoute(routes, basePath, path, search);
9
+ console.debug("[xstate-tree] Match result", match);
10
+ if (match.type === "no-matches") {
11
+ const fourOhFour = {
12
+ type: "ROUTING_404",
13
+ url: path,
14
+ };
15
+ // @ts-ignore the event won't match GlobalEvents
16
+ broadcast(fourOhFour);
17
+ }
18
+ else if (match.type === "match-error") {
19
+ console.error("Error matching route for", location.pathname);
20
+ }
21
+ else {
22
+ const matchedEvent = match.event;
23
+ matchedEvent.meta = { ...(meta ?? {}) };
24
+ matchedEvent.meta.indexEvent = true;
25
+ const { params } = match.event;
26
+ const routingEvents = [];
27
+ let route = match.route;
28
+ while (route.parent) {
29
+ routingEvents.push(route.parent.getEvent({ params, query: {}, meta: { ...(meta ?? {}) } }));
30
+ route = route.parent;
31
+ }
32
+ setActiveRouteEvents([...routingEvents, match.event]);
33
+ // Bumps the processing for the state machines to the next event tick
34
+ // and out of the popstate handler
35
+ setTimeout(() => {
36
+ while (routingEvents.length > 0) {
37
+ const event = routingEvents.pop();
38
+ // copy the originalUrl to all parent events
39
+ event.originalUrl = match.event.originalUrl;
40
+ // @ts-ignore the event won't match GlobalEvents
41
+ broadcast(event);
42
+ }
43
+ // @ts-ignore the event won't match GlobalEvents
44
+ broadcast(matchedEvent);
45
+ }, 0);
46
+ }
47
+ }
@@ -0,0 +1 @@
1
+ export { handleLocationChange, } from "./handleLocationChange";
@@ -0,0 +1,6 @@
1
+ export { buildCreateRoute, } from "./createRoute";
2
+ export { joinRoutes } from "./joinRoutes";
3
+ export { Link } from "./Link";
4
+ export { matchRoute } from "./matchRoute";
5
+ export { handleLocationChange, } from "./handleLocationChange";
6
+ export { RoutingContext } from "./providers";
@@ -0,0 +1,5 @@
1
+ export function joinRoutes(base, route) {
2
+ const realBase = base.endsWith("/") ? base.slice(0, -1) : base;
3
+ const realRoute = route.startsWith("/") ? route : `/${route}`;
4
+ return realBase + realRoute;
5
+ }
@@ -0,0 +1 @@
1
+ export { matchRoute } from "./matchRoute";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @public
3
+ */
4
+ export function matchRoute(routes, basePath, path, search) {
5
+ const realBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
6
+ const realPath = (() => {
7
+ if (path.startsWith(realBase) && realBase.length > 0) {
8
+ return path.substring(realBase.length);
9
+ }
10
+ return path;
11
+ })();
12
+ const [matchingRoute, event] = routes
13
+ .map((route) => {
14
+ try {
15
+ const match = route.matches(realPath, search);
16
+ if (match) {
17
+ return [route, match];
18
+ }
19
+ }
20
+ catch (e) {
21
+ if (e instanceof Error) {
22
+ return [e, undefined];
23
+ }
24
+ }
25
+ return [undefined, undefined];
26
+ })
27
+ .find(([match]) => Boolean(match)) ?? [undefined, undefined];
28
+ if (matchingRoute === undefined) {
29
+ return { type: "no-matches" };
30
+ }
31
+ else if (matchingRoute instanceof Error) {
32
+ return { type: "match-error" };
33
+ }
34
+ return { type: "matched", route: matchingRoute, event: event };
35
+ }
@@ -0,0 +1,18 @@
1
+ import { createContext, useContext } from "react";
2
+ export const RoutingContext = createContext(undefined);
3
+ function useRoutingContext() {
4
+ const context = useContext(RoutingContext);
5
+ if (context === undefined) {
6
+ throw new Error("useRoutingContext must be used within a RoutingContext provider");
7
+ }
8
+ return context;
9
+ }
10
+ export function useActiveRouteEvents() {
11
+ try {
12
+ const context = useRoutingContext();
13
+ return context.activeRouteEvents;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { joinRoutes } from "./joinRoutes";
2
+ /**
3
+ * Returns a string created by joining the base path and routes URL
4
+ */
5
+ export function useHref(to, params, query) {
6
+ try {
7
+ const routePath = to.reverse({ params, query });
8
+ return joinRoutes(to.basePath, routePath);
9
+ }
10
+ catch (e) {
11
+ console.error(e);
12
+ }
13
+ return "";
14
+ }
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/extend-expect";
@@ -0,0 +1 @@
1
+ export * from "./slots";
@@ -0,0 +1,25 @@
1
+ var SlotType;
2
+ (function (SlotType) {
3
+ SlotType[SlotType["SingleSlot"] = 0] = "SingleSlot";
4
+ SlotType[SlotType["MultiSlot"] = 1] = "MultiSlot";
5
+ })(SlotType || (SlotType = {}));
6
+ /**
7
+ * @public
8
+ */
9
+ export function singleSlot(name) {
10
+ return {
11
+ type: SlotType.SingleSlot,
12
+ name,
13
+ getId: () => `${name.toLowerCase()}-slot`,
14
+ };
15
+ }
16
+ /**
17
+ * @public
18
+ */
19
+ export function multiSlot(name) {
20
+ return {
21
+ type: SlotType.MultiSlot,
22
+ name,
23
+ getId: (id) => `${id}-${name.toLowerCase()}-slots`,
24
+ };
25
+ }