@sit-onyx/storybook-utils 1.0.0-beta.3 → 1.0.0-beta.31

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sit-onyx/storybook-utils",
3
3
  "description": "Storybook utilities for Vue",
4
- "version": "1.0.0-beta.3",
4
+ "version": "1.0.0-beta.31",
5
5
  "type": "module",
6
6
  "author": "Schwarz IT KG",
7
7
  "license": "Apache-2.0",
@@ -23,17 +23,14 @@
23
23
  "url": "https://github.com/SchwarzIT/onyx/issues"
24
24
  },
25
25
  "peerDependencies": {
26
- "@storybook/core-events": ">= 8.0.0",
27
- "@storybook/docs-tools": ">= 8.0.0",
28
- "@storybook/preview-api": ">= 8.0.0",
29
- "@storybook/theming": ">= 8.0.0",
30
- "@storybook/vue3": ">= 8.0.0",
26
+ "@storybook/vue3": ">= 8.2.0",
27
+ "storybook": ">= 8.2.0",
31
28
  "storybook-dark-mode": ">= 4",
32
29
  "@sit-onyx/icons": "^1.0.0-beta.0",
33
- "sit-onyx": "^1.0.0-beta.3"
30
+ "sit-onyx": "^1.0.0-beta.30"
34
31
  },
35
32
  "dependencies": {
36
- "deepmerge-ts": "^7.0.3"
33
+ "deepmerge-ts": "^7.1.0"
37
34
  },
38
35
  "scripts": {
39
36
  "build": "tsc --noEmit",
package/src/actions.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { useArgs } from "@storybook/preview-api";
2
1
  import type { ArgTypes, Decorator, Meta } from "@storybook/vue3";
3
2
  import { deepmerge } from "deepmerge-ts";
3
+ import { useArgs } from "storybook/internal/preview-api";
4
4
  import { isReactive, reactive, watch } from "vue";
5
5
  import type { DefineStorybookActionsAndVModelsOptions, ExtractVueEventNames } from ".";
6
6
 
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./actions";
2
2
  export * from "./preview";
3
+ export * from "./sbType";
3
4
  export * from "./theme";
4
5
  export * from "./types";
package/src/preview.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { DOCS_RENDERED } from "@storybook/core-events";
2
- import { addons } from "@storybook/preview-api";
3
- import { type ThemeVars } from "@storybook/theming";
1
+ import { getIconImportName } from "@sit-onyx/icons";
4
2
  import { type Preview, type StoryContext } from "@storybook/vue3";
5
3
  import { deepmerge } from "deepmerge-ts";
6
4
  import { DARK_MODE_EVENT_NAME } from "storybook-dark-mode";
7
-
8
- import { getIconImportName } from "@sit-onyx/icons";
5
+ import { DOCS_RENDERED } from "storybook/internal/core-events";
6
+ import { addons } from "storybook/internal/preview-api";
7
+ import type { ThemeVars } from "storybook/internal/theming";
9
8
  import { requiredGlobalType, withRequired } from "./required";
10
9
  import { generateSourceCode } from "./source-code-generator";
11
10
  import { ONYX_BREAKPOINTS, createTheme } from "./theme";
@@ -0,0 +1,38 @@
1
+ import type { SBType } from "storybook/internal/types";
2
+ import { describe, expect, test, vi } from "vitest";
3
+ import { walkTree } from "./sbType";
4
+
5
+ describe("walkTree", () => {
6
+ test.each<{ input: SBType; expected: SBType["name"][] }>([
7
+ { input: { name: "array", value: { name: "number" } }, expected: ["array", "number"] },
8
+ { input: { name: "object", value: { a: { name: "number" } } }, expected: ["object", "number"] },
9
+ { input: { name: "enum", value: ["a"] }, expected: ["enum"] },
10
+ {
11
+ input: { name: "intersection", value: [{ name: "number" }] },
12
+ expected: ["intersection", "number"],
13
+ },
14
+ { input: { name: "union", value: [{ name: "number" }] }, expected: ["union", "number"] },
15
+ { input: { name: "other", value: "a" }, expected: ["other"] },
16
+ ])("should execute cb for $input.name correctly", ({ input, expected }) => {
17
+ const spy = vi.fn<(p: SBType) => void>();
18
+ const result = walkTree(input, spy);
19
+
20
+ expect(result).toBeUndefined();
21
+ expect(spy).toHaveBeenCalledTimes(expected.length);
22
+ const nameCalls = spy.mock.calls.map(([{ name }]) => name);
23
+ expect(nameCalls).toMatchObject(expected);
24
+ });
25
+
26
+ test("should return value if there is any returned", () => {
27
+ const target: SBType = { name: "number", raw: "here" };
28
+ const overshoot: SBType = { name: "boolean", raw: "here" };
29
+ const parent: SBType = { name: "intersection", value: [target, overshoot] };
30
+ const returned = 42;
31
+ const spy = vi.fn((p: SBType) => (p.raw === "here" ? returned : undefined));
32
+ const result = walkTree({ name: "union", value: [parent] }, spy);
33
+
34
+ expect(spy).toHaveBeenCalledTimes(3);
35
+ expect(spy).toHaveBeenLastCalledWith(target, parent);
36
+ expect(result).toBe(returned);
37
+ });
38
+ });
package/src/sbType.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { SBType } from "storybook/internal/types";
2
+
3
+ /**
4
+ * Call a function `cb` for every type node in the storybook type tree.
5
+ * @param inputType the root type
6
+ * @param cb the function that is called for every type. If any non-nullish value is returned by `cb` the execution is stopped and this value is returned.
7
+ * @param parent optional, the parent type. Is only used as input for the `cb` function and provided when recursing.
8
+ * @returns the first non-nullish value that is returned by `cb`
9
+ */
10
+ export const walkTree = <TValue>(
11
+ inputType: SBType,
12
+ cb: (sb: SBType, parent?: SBType) => TValue,
13
+ parent?: SBType,
14
+ ): TValue | undefined => {
15
+ const shouldReturn = cb(inputType, parent);
16
+ if (shouldReturn) {
17
+ return shouldReturn;
18
+ }
19
+
20
+ if (inputType.name === "union" || inputType.name === "intersection") {
21
+ return inputType.value.reduce<TValue | undefined>(
22
+ (prev, it) => prev ?? walkTree(it, cb, inputType),
23
+ undefined,
24
+ );
25
+ }
26
+ if (inputType.name === "array") {
27
+ return walkTree(inputType.value, cb, inputType);
28
+ }
29
+ if (inputType.name === "object") {
30
+ return Object.values(inputType.value).reduce<TValue | undefined>(
31
+ (prev, it) => prev ?? walkTree(it, cb, inputType),
32
+ undefined,
33
+ );
34
+ }
35
+ };
@@ -156,16 +156,20 @@ test("should generate source code for slots with bindings", () => {
156
156
  type TestBindings = {
157
157
  foo: string;
158
158
  bar?: number;
159
+ boo: {
160
+ mimeType: string;
161
+ };
159
162
  };
160
163
 
161
164
  const slots = {
162
- a: ({ foo, bar }: TestBindings) => `Slot with bindings ${foo} and ${bar}`,
163
- b: ({ foo }: TestBindings) => h("a", { href: foo, target: foo }, `Test link: ${foo}`),
165
+ a: ({ foo, bar, boo }: TestBindings) => `Slot with bindings ${foo}, ${bar} and ${boo.mimeType}`,
166
+ b: ({ foo, boo }: TestBindings) =>
167
+ h("a", { href: foo, target: foo, type: boo.mimeType, ...boo }, `Test link: ${foo}`),
164
168
  };
165
169
 
166
- const expectedCode = `<template #a="{ foo, bar }">Slot with bindings {{ foo }} and {{ bar }}</template>
170
+ const expectedCode = `<template #a="{ foo, bar, boo }">Slot with bindings {{ foo }}, {{ bar }} and {{ boo.mimeType }}</template>
167
171
 
168
- <template #b="{ foo }"><a :href="foo" :target="foo">Test link: {{ foo }}</a></template>`;
172
+ <template #b="{ foo, boo }"><a :href="foo" :target="foo" :type="boo.mimeType" v-bind="boo">Test link: {{ foo }}</a></template>`;
169
173
 
170
174
  const actualCode = generateSlotSourceCode(slots, Object.keys(slots), {
171
175
  imports: {},
@@ -3,11 +3,25 @@
3
3
  // It is intended to be deleted once its officially released in Storybook itself, see:
4
4
  // https://github.com/storybookjs/storybook/pull/27194
5
5
  //
6
- import { SourceType } from "@storybook/docs-tools";
7
6
  import type { Args, StoryContext } from "@storybook/vue3";
7
+ import { SourceType } from "storybook/internal/docs-tools";
8
8
  import { isVNode, type VNode } from "vue";
9
9
  import { replaceAll } from "./preview";
10
10
 
11
+ /**
12
+ * Used to get the tracking data from the proxy.
13
+ * A symbol is unique, so when using it as a key it can't be accidentally accessed.
14
+ */
15
+ const TRACKING_SYMBOL = Symbol("DEEP_ACCESS_SYMBOL");
16
+
17
+ type TrackingProxy = {
18
+ [TRACKING_SYMBOL]: true;
19
+ toString: () => string;
20
+ };
21
+
22
+ const isProxy = (obj: unknown): obj is TrackingProxy =>
23
+ !!(obj && typeof obj === "object" && TRACKING_SYMBOL in obj);
24
+
11
25
  /**
12
26
  * Context that is passed down to nested components/slots when generating the source code for a single story.
13
27
  */
@@ -184,6 +198,10 @@ export const generatePropsSourceCode = (
184
198
  if (slotNames.includes(propName)) return;
185
199
  if (value == undefined) return; // do not render undefined/null values
186
200
 
201
+ if (isProxy(value)) {
202
+ value = value!.toString();
203
+ }
204
+
187
205
  switch (typeof value) {
188
206
  case "string":
189
207
  if (value === "") return; // do not render empty strings
@@ -225,7 +243,7 @@ export const generatePropsSourceCode = (
225
243
  case "object": {
226
244
  properties.push({
227
245
  name: propName,
228
- value: formatObject(value),
246
+ value: formatObject(value ?? {}),
229
247
  // to follow Vue best practices, complex values like object and arrays are
230
248
  // usually placed inside the <script setup> block instead of inlining them in the <template>
231
249
  templateFn: undefined,
@@ -373,25 +391,60 @@ const generateSlotChildrenSourceCode = (
373
391
  (param) => !["{", "}"].includes(param),
374
392
  );
375
393
 
376
- const parameters = paramNames.reduce<Record<string, string>>((obj, param) => {
377
- obj[param] = `{{ ${param} }}`;
378
- return obj;
379
- }, {});
380
-
381
- const returnValue = child(parameters);
382
- let slotSourceCode = generateSlotChildrenSourceCode([returnValue], ctx);
383
-
384
- // if slot bindings are used for properties of other components, our {{ paramName }} is incorrect because
385
- // it would generate e.g. my-prop="{{ paramName }}", therefore, we replace it here to e.g. :my-prop="paramName"
394
+ // We create proxy to track how and which properties of a parameter are accessed
395
+ const parameters: Record<string, string> = {};
396
+ const proxied: Record<string, TrackingProxy> = {};
386
397
  paramNames.forEach((param) => {
387
- slotSourceCode = replaceAll(
388
- slotSourceCode,
389
- new RegExp(` (\\S+)="{{ ${param} }}"`, "g"),
390
- ` :$1="${param}"`,
398
+ parameters[param] = `{{ ${param} }}`;
399
+ // TODO: we should be able to extend the proxy logic here and maybe get rid of the `generatePropsSourceCode` code
400
+ proxied[param] = new Proxy(
401
+ {
402
+ // we use the symbol to identify the proxy
403
+ [TRACKING_SYMBOL]: true,
404
+ } as TrackingProxy,
405
+ {
406
+ // getter is called when any prop of the parameter is read
407
+ get: (t, key) => {
408
+ if (key === TRACKING_SYMBOL) {
409
+ // allow retrieval of the tracking data
410
+ return t[TRACKING_SYMBOL];
411
+ }
412
+ if ([Symbol.toPrimitive, Symbol.toStringTag, "toString"].includes(key)) {
413
+ // when the parameter is used as a string we return the parameter name
414
+ // we use the double brace notation as we don't know if the parameter is used in text or in a binding
415
+ return () => `{{ ${param} }}`;
416
+ }
417
+ if (key === "v-bind") {
418
+ // if this key is returned we just return the parameter name
419
+ return `${param}`;
420
+ }
421
+ // otherwise a specific key of the parameter was accessed
422
+ // we use the double brace notation as we don't know if the parameter is used in text or in a binding
423
+ return `{{ ${param}.${key.toString()} }}`;
424
+ },
425
+ // ownKeys is called, among other uses, when an object is destructured
426
+ // in this case we assume the parameter is supposed to be bound using "v-bind"
427
+ // Therefore we only return one special key "v-bind" and the getter will be called afterwards with it
428
+ ownKeys: () => {
429
+ return [`v-bind`];
430
+ },
431
+ /** called when destructured */
432
+ getOwnPropertyDescriptor: () => ({
433
+ configurable: true,
434
+ enumerable: true,
435
+ value: param,
436
+ writable: true,
437
+ }),
438
+ },
391
439
  );
392
440
  });
393
441
 
394
- return slotSourceCode;
442
+ const returnValue = child(proxied);
443
+ const slotSourceCode = generateSlotChildrenSourceCode([returnValue], ctx);
444
+
445
+ // if slot bindings are used for properties of other components, our {{ paramName }} is incorrect because
446
+ // it would generate e.g. my-prop="{{ paramName }}", therefore, we replace it here to e.g. :my-prop="paramName"
447
+ return replaceAll(slotSourceCode, / (\S+)="{{ (\S+) }}"/g, ` :$1="$2"`);
395
448
  }
396
449
 
397
450
  case "bigint":
package/src/theme.ts CHANGED
@@ -1,7 +1,6 @@
1
- import type { ThemeVars, ThemeVarsPartial } from "@storybook/theming";
2
- import { create } from "@storybook/theming/create";
1
+ import { ONYX_BREAKPOINTS as RAW_ONYX_BREAKPOINTS, type OnyxBreakpoint } from "sit-onyx";
3
2
  import onyxVariables from "sit-onyx/themes/onyx.json";
4
- import { ONYX_BREAKPOINTS as RAW_ONYX_BREAKPOINTS, type OnyxBreakpoint } from "sit-onyx/types";
3
+ import { create, type ThemeVars, type ThemeVarsPartial } from "storybook/internal/theming";
5
4
  import onyxLogo from "./assets/logo-onyx.svg";
6
5
 
7
6
  /**
package/src/types.ts CHANGED
@@ -55,5 +55,6 @@ export type StorybookGlobalType<TValue> = {
55
55
  icon: string;
56
56
  items: { value: TValue; right?: string; title: string }[];
57
57
  title?: string;
58
+ dynamicTitle?: boolean;
58
59
  };
59
60
  };