@storybook/react-native-ui-common 9.0.0-beta.15

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,365 @@
1
+ import { sanitize } from '@storybook/csf';
2
+ import type { API, State } from 'storybook/internal/manager-api';
3
+ import type {
4
+ API_ComponentEntry,
5
+ API_DocsEntry,
6
+ API_GroupEntry,
7
+ API_HashEntry,
8
+ API_IndexHash,
9
+ API_PreparedStoryIndex,
10
+ API_Provider,
11
+ API_RootEntry,
12
+ API_StoryEntry,
13
+ DocsOptions,
14
+ IndexEntry,
15
+ StatusesByStoryIdAndTypeId,
16
+ StoryIndexV2,
17
+ StoryIndexV3,
18
+ Tag,
19
+ } from 'storybook/internal/types';
20
+ import { dedent } from 'ts-dedent';
21
+ import { logger } from 'storybook/internal/client-logger';
22
+ import { countBy, isEqual, mergeWith } from 'es-toolkit';
23
+
24
+ type ToStoriesHashOptions = {
25
+ provider: API_Provider<API>;
26
+ docsOptions: DocsOptions;
27
+ filters: State['filters'];
28
+ allStatuses: StatusesByStoryIdAndTypeId;
29
+ };
30
+ export const intersect = <T>(a: T[], b: T[]): T[] => {
31
+ // no point in intersecting if one of the input is ill-defined
32
+ if (!Array.isArray(a) || !Array.isArray(b) || !a.length || !b.length) {
33
+ return [];
34
+ }
35
+
36
+ return a.reduce((acc: T[], aValue) => {
37
+ if (b.includes(aValue)) {
38
+ acc.push(aValue);
39
+ }
40
+
41
+ return acc;
42
+ }, []);
43
+ };
44
+
45
+ export const merge = <TObj = any>(a: TObj, ...b: Partial<TObj>[]): TObj => {
46
+ // start with empty object
47
+ let target = {};
48
+
49
+ // merge object a unto target
50
+ target = mergeWith(
51
+ {},
52
+ a as Record<PropertyKey, any>,
53
+ (objValue: TObj, srcValue: Partial<TObj>) => {
54
+ if (Array.isArray(srcValue) && Array.isArray(objValue)) {
55
+ srcValue.forEach((s) => {
56
+ const existing = objValue.find((o) => o === s || isEqual(o, s));
57
+ if (!existing) {
58
+ objValue.push(s);
59
+ }
60
+ });
61
+
62
+ return objValue;
63
+ }
64
+ if (Array.isArray(objValue)) {
65
+ logger.log(['the types mismatch, picking', objValue]);
66
+ return objValue;
67
+ }
68
+ }
69
+ );
70
+
71
+ for (const obj of b) {
72
+ // merge object b unto target
73
+ target = mergeWith(target, obj, (objValue: TObj, srcValue: Partial<TObj>) => {
74
+ if (Array.isArray(srcValue) && Array.isArray(objValue)) {
75
+ srcValue.forEach((s) => {
76
+ const existing = objValue.find((o) => o === s || isEqual(o, s));
77
+ if (!existing) {
78
+ objValue.push(s);
79
+ }
80
+ });
81
+
82
+ return objValue;
83
+ }
84
+ if (Array.isArray(objValue)) {
85
+ logger.log(['the types mismatch, picking', objValue]);
86
+ return objValue;
87
+ }
88
+ });
89
+ }
90
+
91
+ return target as TObj;
92
+ };
93
+
94
+ export const noArrayMerge = <TObj = any>(a: TObj, ...b: Partial<TObj>[]): TObj => {
95
+ // start with empty object
96
+ let target = {};
97
+
98
+ // merge object a unto target
99
+ target = mergeWith(
100
+ {},
101
+ a as Record<PropertyKey, any>,
102
+ (objValue: TObj, srcValue: Partial<TObj>) => {
103
+ // Treat arrays as scalars:
104
+ if (Array.isArray(srcValue)) {
105
+ return srcValue;
106
+ }
107
+ }
108
+ );
109
+
110
+ for (const obj of b) {
111
+ // merge object b unto target
112
+ target = mergeWith(target, obj, (objValue: TObj, srcValue: Partial<TObj>) => {
113
+ // Treat arrays as scalars:
114
+ if (Array.isArray(srcValue)) {
115
+ return srcValue;
116
+ }
117
+ });
118
+ }
119
+
120
+ return target as TObj;
121
+ };
122
+
123
+ const TITLE_PATH_SEPARATOR = /\s*\/\s*/;
124
+
125
+ export const transformStoryIndexToStoriesHash = (
126
+ input: API_PreparedStoryIndex | StoryIndexV2 | StoryIndexV3,
127
+ { provider, docsOptions, filters, allStatuses }: ToStoriesHashOptions
128
+ ): API_IndexHash | any => {
129
+ if (!input.v) {
130
+ throw new Error('Composition: Missing stories.json version');
131
+ }
132
+
133
+ let index = input;
134
+ index = index.v === 2 ? transformStoryIndexV2toV3(index as any) : index;
135
+ index = index.v === 3 ? transformStoryIndexV3toV4(index as any) : index;
136
+ index = index.v === 4 ? transformStoryIndexV4toV5(index as any) : index;
137
+ index = index as API_PreparedStoryIndex;
138
+
139
+ const entryValues = Object.values(index.entries).filter((entry: any) => {
140
+ let result = true;
141
+
142
+ // All stories with a failing status should always show up, regardless of the applied filters
143
+ const storyStatuses = allStatuses[entry.id] ?? {};
144
+ if (Object.values(storyStatuses).some(({ value }) => value === 'status-value:error')) {
145
+ return result;
146
+ }
147
+
148
+ Object.values(filters).forEach((filter) => {
149
+ if (result === false) {
150
+ return;
151
+ }
152
+ result = filter({ ...entry, statuses: storyStatuses });
153
+ });
154
+
155
+ return result;
156
+ });
157
+
158
+ const { sidebar = {} } = provider.getConfig();
159
+ const { showRoots, collapsedRoots = [], renderLabel }: any = sidebar;
160
+
161
+ const setShowRoots = typeof showRoots !== 'undefined';
162
+
163
+ const storiesHashOutOfOrder = entryValues.reduce((acc: any, item: any) => {
164
+ if (docsOptions.docsMode && item.type !== 'docs') {
165
+ return acc;
166
+ }
167
+
168
+ // First, split the title into a set of names, separated by '/' and trimmed.
169
+ const { title } = item;
170
+ const groups = title.trim().split(TITLE_PATH_SEPARATOR);
171
+ const root = (!setShowRoots || showRoots) && groups.length > 1 ? [groups.shift()] : [];
172
+ const names = [...root, ...groups];
173
+
174
+ // Now create a "path" or sub id for each name
175
+ const paths = names.reduce((list, name, idx) => {
176
+ const parent = idx > 0 && list[idx - 1];
177
+ const id = sanitize(parent ? `${parent}-${name}` : name!);
178
+
179
+ if (name.trim() === '') {
180
+ throw new Error(dedent`Invalid title ${title} ending in slash.`);
181
+ }
182
+
183
+ if (parent === id) {
184
+ throw new Error(
185
+ dedent`
186
+ Invalid part '${name}', leading to id === parentId ('${id}'), inside title '${title}'
187
+
188
+ Did you create a path that uses the separator char accidentally, such as 'Vue <docs/>' where '/' is a separator char? See https://github.com/storybookjs/storybook/issues/6128
189
+ `
190
+ );
191
+ }
192
+ list.push(id);
193
+ return list;
194
+ }, [] as string[]);
195
+
196
+ // Now, let's add an entry to the hash for each path/name pair
197
+ paths.forEach((id: any, idx: any) => {
198
+ // The child is the next path, OR the story/docs entry itself
199
+ const childId = paths[idx + 1] || item.id;
200
+
201
+ if (root.length && idx === 0) {
202
+ acc[id] = merge<API_RootEntry>((acc[id] || {}) as API_RootEntry, {
203
+ type: 'root',
204
+ id,
205
+ name: names[idx],
206
+ tags: [],
207
+ depth: idx,
208
+ renderLabel,
209
+ startCollapsed: collapsedRoots.includes(id),
210
+ // Note that this will later get appended to the previous list of children (see below)
211
+ children: [childId],
212
+ });
213
+ // Usually the last path/name pair will be displayed as a component,
214
+ // *unless* there are other stories that are more deeply nested under it
215
+ //
216
+ // For example, if we had stories for both
217
+ // - Atoms / Button
218
+ // - Atoms / Button / LabelledButton
219
+ //
220
+ // In this example the entry for 'atoms-button' would *not* be a component.
221
+ } else if ((!acc[id] || acc[id].type === 'component') && idx === paths.length - 1) {
222
+ acc[id] = merge<API_ComponentEntry>((acc[id] || {}) as API_ComponentEntry, {
223
+ type: 'component',
224
+ id,
225
+ name: names[idx],
226
+ tags: [],
227
+ parent: paths[idx - 1],
228
+ depth: idx,
229
+ renderLabel,
230
+ ...(childId && {
231
+ children: [childId],
232
+ }),
233
+ });
234
+ } else {
235
+ acc[id] = merge<API_GroupEntry>((acc[id] || {}) as API_GroupEntry, {
236
+ type: 'group',
237
+ id,
238
+ name: names[idx],
239
+ tags: [],
240
+ parent: paths[idx - 1],
241
+ depth: idx,
242
+ renderLabel,
243
+ ...(childId && {
244
+ children: [childId],
245
+ }),
246
+ });
247
+ }
248
+ });
249
+
250
+ // Finally add an entry for the docs/story itself
251
+ acc[item.id] = {
252
+ type: 'story',
253
+ tags: [],
254
+ ...item,
255
+ depth: paths.length,
256
+ parent: paths[paths.length - 1],
257
+ renderLabel,
258
+ prepared: !!item.parameters,
259
+ } as API_DocsEntry | API_StoryEntry;
260
+
261
+ return acc;
262
+ }, {} as API_IndexHash);
263
+
264
+ // This function adds a "root" or "orphan" and all of its descendents to the hash.
265
+ function addItem(acc: API_IndexHash | any, item: API_HashEntry | any) {
266
+ // If we were already inserted as part of a group, that's great.
267
+ if (acc[item.id]) {
268
+ return acc;
269
+ }
270
+
271
+ acc[item.id] = item;
272
+ // Ensure we add the children depth-first *before* inserting any other entries,
273
+ // and compute tags from the children put in the accumulator afterwards, once
274
+ // they're all known and we can compute a sound intersection.
275
+ if (item.type === 'root' || item.type === 'group' || item.type === 'component') {
276
+ item.children.forEach((childId: any) => addItem(acc, storiesHashOutOfOrder[childId]));
277
+
278
+ item.tags = item.children.reduce((currentTags: Tag[] | null, childId: any): Tag[] => {
279
+ const child = acc[childId];
280
+
281
+ // On the first child, we have nothing to intersect against so we use it as a source of data.
282
+ return currentTags === null ? child.tags : intersect(currentTags, child.tags);
283
+ }, null);
284
+ }
285
+ return acc;
286
+ }
287
+
288
+ // We'll do two passes over the data, adding all the orphans, then all the roots
289
+ const orphanHash = Object.values(storiesHashOutOfOrder)
290
+ .filter((i: any) => i.type !== 'root' && !i.parent)
291
+ .reduce(addItem, {});
292
+
293
+ return Object.values(storiesHashOutOfOrder)
294
+ .filter((i: any) => i.type === 'root')
295
+ .reduce(addItem, orphanHash);
296
+ };
297
+
298
+ export const transformStoryIndexV2toV3 = (index: StoryIndexV2): StoryIndexV3 => {
299
+ return {
300
+ v: 3,
301
+ stories: Object.values(index.stories).reduce(
302
+ (acc, entry) => {
303
+ acc[entry.id] = {
304
+ ...entry,
305
+ title: entry.kind,
306
+ name: entry.name || entry.story,
307
+ importPath: entry.parameters.fileName || '',
308
+ };
309
+
310
+ return acc;
311
+ },
312
+ {} as StoryIndexV3['stories']
313
+ ),
314
+ };
315
+ };
316
+
317
+ export const transformStoryIndexV3toV4 = (index: StoryIndexV3): API_PreparedStoryIndex => {
318
+ const countByTitle = countBy(Object.values(index.stories), (item) => item.title);
319
+ return {
320
+ v: 4,
321
+ entries: Object.values(index.stories).reduce(
322
+ (acc, entry: any) => {
323
+ let type: IndexEntry['type'] = 'story';
324
+ if (
325
+ entry.parameters?.docsOnly ||
326
+ (entry.name === 'Page' && countByTitle[entry.title] === 1)
327
+ ) {
328
+ type = 'docs';
329
+ }
330
+ acc[entry.id] = {
331
+ type,
332
+ ...(type === 'docs' && { tags: ['stories-mdx'], storiesImports: [] }),
333
+ ...entry,
334
+ };
335
+
336
+ // @ts-expect-error (we're removing something that should not be there)
337
+ delete acc[entry.id].story;
338
+ // @ts-expect-error (we're removing something that should not be there)
339
+ delete acc[entry.id].kind;
340
+
341
+ return acc;
342
+ },
343
+ {} as API_PreparedStoryIndex['entries']
344
+ ),
345
+ };
346
+ };
347
+
348
+ export const transformStoryIndexV4toV5 = (
349
+ index: API_PreparedStoryIndex
350
+ ): API_PreparedStoryIndex => {
351
+ return {
352
+ v: 5,
353
+ entries: Object.values(index.entries).reduce(
354
+ (acc, entry) => {
355
+ acc[entry.id] = {
356
+ ...entry,
357
+ tags: entry.tags ? ['dev', 'test', ...entry.tags] : ['dev'],
358
+ };
359
+
360
+ return acc;
361
+ },
362
+ {} as API_PreparedStoryIndex['entries']
363
+ ),
364
+ };
365
+ };
@@ -0,0 +1,3 @@
1
+ export * from './StoryHash';
2
+ export * from './tree';
3
+ export * from './useStyle';
@@ -0,0 +1,93 @@
1
+ import memoize from 'memoizerific';
2
+ import type { SyntheticEvent } from 'react';
3
+ import type { IndexHash } from 'storybook/internal/manager-api';
4
+
5
+ import { DEFAULT_REF_ID } from '../constants';
6
+ import type { Item, RefType, Dataset, SearchItem } from '../types';
7
+
8
+ export const createId = (itemId: string, refId?: string) =>
9
+ !refId || refId === DEFAULT_REF_ID ? itemId : `${refId}_${itemId}`;
10
+
11
+ export const prevent = (e: SyntheticEvent) => {
12
+ e.preventDefault();
13
+ return false;
14
+ };
15
+
16
+ export const get = memoize(1000)((id: string, dataset: Dataset) => dataset[id]);
17
+
18
+ export const getParent = memoize(1000)((id: string, dataset: Dataset) => {
19
+ const item = get(id, dataset);
20
+ return item && item.type !== 'root' ? get(item.parent, dataset) : undefined;
21
+ });
22
+
23
+ export const getParents = memoize(1000)((id: string, dataset: Dataset): Item[] => {
24
+ const parent = getParent(id, dataset);
25
+ return parent ? [parent, ...getParents(parent.id, dataset)] : [];
26
+ });
27
+
28
+ export const getAncestorIds = memoize(1000)((data: IndexHash, id: string): string[] =>
29
+ getParents(id, data).map((item) => item.id)
30
+ );
31
+
32
+ export const getDescendantIds = memoize(1000)((
33
+ data: IndexHash,
34
+ id: string,
35
+ skipLeafs: boolean
36
+ ): string[] => {
37
+ const entry = data[id];
38
+ const children = entry.type === 'story' || entry.type === 'docs' ? [] : entry.children;
39
+ return children.reduce((acc, childId) => {
40
+ const child = data[childId];
41
+ if (!child || (skipLeafs && (child.type === 'story' || child.type === 'docs'))) return acc;
42
+ acc.push(childId, ...getDescendantIds(data, childId, skipLeafs));
43
+ return acc;
44
+ }, []);
45
+ });
46
+
47
+ export function getPath(item: Item, ref: RefType): string[] {
48
+ const parent = item.type !== 'root' && item.parent ? ref.index[item.parent] : null;
49
+ if (parent) return [...getPath(parent, ref), parent.name];
50
+ return ref.id === DEFAULT_REF_ID ? [] : [ref.title || ref.id];
51
+ }
52
+
53
+ export const searchItem = (item: Item, ref: RefType): SearchItem => {
54
+ return { ...item, refId: ref.id, path: getPath(item, ref) };
55
+ };
56
+
57
+ export function cycle<T>(array: T[], index: number, delta: number): number {
58
+ let next = index + (delta % array.length);
59
+ if (next < 0) next = array.length + next;
60
+ if (next >= array.length) next -= array.length;
61
+ return next;
62
+ }
63
+
64
+ export const getStateType = (
65
+ isLoading: boolean,
66
+ isAuthRequired: boolean,
67
+ isError: boolean,
68
+ isEmpty: boolean
69
+ ) => {
70
+ switch (true) {
71
+ case isAuthRequired:
72
+ return 'auth';
73
+ case isError:
74
+ return 'error';
75
+ case isLoading:
76
+ return 'loading';
77
+ case isEmpty:
78
+ return 'empty';
79
+ default:
80
+ return 'ready';
81
+ }
82
+ };
83
+
84
+ export const isAncestor = (element?: Element, maybeAncestor?: Element): boolean => {
85
+ if (!element || !maybeAncestor) return false;
86
+ if (element === maybeAncestor) return true;
87
+ return isAncestor(element.parentElement, maybeAncestor);
88
+ };
89
+
90
+ export const removeNoiseFromName = (storyName: string) => storyName.replaceAll(/(\s|-|_)/gi, '');
91
+
92
+ export const isStoryHoistable = (storyName: string, componentName: string) =>
93
+ removeNoiseFromName(storyName) === removeNoiseFromName(componentName);
@@ -0,0 +1,28 @@
1
+ import { DependencyList, useMemo } from 'react';
2
+ import { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
3
+
4
+ /**
5
+ * A hook to memoize a style. Uses `ViewStyle` per default, but can be used with other styles deriving from `FlexStyle` as well, such as `TextStyle`.
6
+ * @param styleFactory The function that returns a style
7
+ * @param deps The dependencies to trigger memoization re-evaluation
8
+ * @see ["Memoize!!! 💾 - a react (native) performance guide"](https://gist.github.com/mrousavy/0de7486814c655de8a110df5cef74ddc)
9
+ * @example
10
+ *
11
+ * // simple object styles
12
+ * const style1 = useStyle(() => ({ height: someDynamicValue }), [someDynamicValue])
13
+ *
14
+ * // array styles
15
+ * const style2 = useStyle(
16
+ * () => [styles.container, props.style, { height: someDynamicValue }],
17
+ * [props.style, someDynamicValue]
18
+ * );
19
+ */
20
+ export const useStyle = <
21
+ TStyle extends ViewStyle | TextStyle | ImageStyle,
22
+ TOutput extends StyleProp<TStyle>,
23
+ >(
24
+ styleFactory: () => TOutput,
25
+ deps?: DependencyList
26
+ ): TOutput =>
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ useMemo(styleFactory, deps);