@repobuddy/storybook 2.28.1 → 2.30.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.
- package/esm/index.d.ts +71 -1
- package/esm/index.js +139 -5
- package/package.json +5 -4
- package/src/decorators/show_source.tsx +5 -3
- package/src/index.ts +1 -0
- package/src/parameters/create_story_sort.ts +216 -0
- package/src/parameters/story_sort.ts +1 -0
- package/styles.css +1 -1
package/esm/index.d.ts
CHANGED
|
@@ -121,6 +121,7 @@ type ShowSourceOptions = Pick<StoryCardProps, 'className' | 'data-testid'> & {
|
|
|
121
121
|
source?: ((source: string | undefined) => string) | string | undefined;
|
|
122
122
|
showOriginalSource?: boolean | undefined;
|
|
123
123
|
placement?: 'before' | 'after' | undefined;
|
|
124
|
+
language?: 'json' | 'md' | 'html' | 'css' | 'js' | 'ts' | (string & {}) | undefined;
|
|
124
125
|
};
|
|
125
126
|
/**
|
|
126
127
|
* A decorator that shows the source code of a story relative to the rendered story.
|
|
@@ -138,6 +139,7 @@ declare function showSource<TRenderer extends Renderer = Renderer, TArgs = Args>
|
|
|
138
139
|
placement,
|
|
139
140
|
showOriginalSource,
|
|
140
141
|
source,
|
|
142
|
+
language,
|
|
141
143
|
...options
|
|
142
144
|
}?: ShowSourceOptions): DecoratorFunction<TRenderer, TArgs>;
|
|
143
145
|
//#endregion
|
|
@@ -267,6 +269,73 @@ declare function withStoryCard<TRenderer extends Renderer = Renderer>({
|
|
|
267
269
|
...rest
|
|
268
270
|
}?: WithStoryCardProps): DecoratorFunction<TRenderer>;
|
|
269
271
|
//#endregion
|
|
272
|
+
//#region src/parameters/create_story_sort.d.ts
|
|
273
|
+
type Story$1 = {
|
|
274
|
+
id: string;
|
|
275
|
+
importPath: string;
|
|
276
|
+
name: string;
|
|
277
|
+
title: string;
|
|
278
|
+
tags?: string[] | undefined;
|
|
279
|
+
};
|
|
280
|
+
type StorySortFn$1 = (a: Story$1, b: Story$1) => number;
|
|
281
|
+
/**
|
|
282
|
+
* A nested ordering list. Each entry is either:
|
|
283
|
+
* - A string (matches a title segment or tag name; `'*'` is the wildcard)
|
|
284
|
+
* - A tuple `[name, children]` for nested sub-ordering (order only)
|
|
285
|
+
*/
|
|
286
|
+
type OrderList = Array<string | [string, OrderList]>;
|
|
287
|
+
interface StorySortOptions {
|
|
288
|
+
/**
|
|
289
|
+
* Ordered list of title path segments (root-level categories).
|
|
290
|
+
* Supports nesting via tuples and `'*'` wildcard.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```ts
|
|
294
|
+
* order: ['Overview', 'components', ['Pages', ['Home', 'Login']], '*', 'WIP']
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
order?: OrderList | undefined;
|
|
298
|
+
/**
|
|
299
|
+
* Ordered list of story category tags.
|
|
300
|
+
* Stories sharing the same title are sorted by their first matching tag.
|
|
301
|
+
* `'*'` marks where stories with unlisted tags sort.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* tag: ['playground', 'use-case', 'spec', 'props', '*', 'unit', 'integration']
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
tag?: OrderList | undefined;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Creates a story sort comparator for Storybook that sorts by kind (title hierarchy)
|
|
312
|
+
* and then by tag category within the same component.
|
|
313
|
+
*
|
|
314
|
+
* In Storybook 10+, `storySort` must be defined as an inline function.
|
|
315
|
+
* Assign the result to a variable and delegate from the inline function:
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```ts
|
|
319
|
+
* import { createStorySort } from '@repobuddy/storybook'
|
|
320
|
+
*
|
|
321
|
+
* const compare = createStorySort({
|
|
322
|
+
* order: ['Overview', 'components', 'hooks', '*', 'widgets'],
|
|
323
|
+
* tag: ['playground', 'use-case', 'example', 'spec', 'props', '*', 'edge-case', 'unit']
|
|
324
|
+
* })
|
|
325
|
+
*
|
|
326
|
+
* export default {
|
|
327
|
+
* parameters: {
|
|
328
|
+
* options: {
|
|
329
|
+
* storySort(a, b) {
|
|
330
|
+
* return compare(a, b)
|
|
331
|
+
* }
|
|
332
|
+
* }
|
|
333
|
+
* }
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
declare function createStorySort(options?: StorySortOptions): StorySortFn$1;
|
|
338
|
+
//#endregion
|
|
270
339
|
//#region src/parameters/define_actions_param.d.ts
|
|
271
340
|
interface ActionsParam {
|
|
272
341
|
actions: {
|
|
@@ -708,6 +777,7 @@ type Story = {
|
|
|
708
777
|
importPath: string;
|
|
709
778
|
name: string;
|
|
710
779
|
title: string;
|
|
780
|
+
tags?: string[] | undefined;
|
|
711
781
|
};
|
|
712
782
|
type StorySortFn = (a: Story, b: Story) => number;
|
|
713
783
|
/**
|
|
@@ -967,4 +1037,4 @@ type ExtendStoryObj<TMetaOrCmpOrArgs, S extends StoryObj<TMetaOrCmpOrArgs>, E ex
|
|
|
967
1037
|
tags?: Array<E['tag'] | (string & {})> | undefined;
|
|
968
1038
|
};
|
|
969
1039
|
//#endregion
|
|
970
|
-
export { ActionsParam, BackgroundsParam, DocsParam, ExtendMeta, ExtendStoryObj, ExtendsMeta, ExtendsStoryObj, FnToArgTypes, GlobalApiBackgroundsParam, GlobalApiViewportParam, LayoutParam, ShowHtml, ShowHtmlProps, ShowSourceOptions, SourceProps, StoryCard, StoryCardAppearance, StoryCardParam, StoryCardProps, StoryCardStatus, StoryCardThemeState, StorySortParam, StorybookBuiltInParams, TestParam, Viewport, ViewportParam, WithStoryCardProps, defineActionsParam, defineBackgroundsParam, defineDocsParam, defineLayoutParam, defineParameters, defineStoryCardParam, defineTestParam, defineViewportParam, showDocSource, showSource, whenRunningInTest, withStoryCard };
|
|
1040
|
+
export { ActionsParam, BackgroundsParam, DocsParam, ExtendMeta, ExtendStoryObj, ExtendsMeta, ExtendsStoryObj, FnToArgTypes, GlobalApiBackgroundsParam, GlobalApiViewportParam, LayoutParam, OrderList, ShowHtml, ShowHtmlProps, ShowSourceOptions, SourceProps, StoryCard, StoryCardAppearance, StoryCardParam, StoryCardProps, StoryCardStatus, StoryCardThemeState, StorySortOptions, StorySortParam, StorybookBuiltInParams, TestParam, Viewport, ViewportParam, WithStoryCardProps, createStorySort, defineActionsParam, defineBackgroundsParam, defineDocsParam, defineLayoutParam, defineParameters, defineStoryCardParam, defineTestParam, defineViewportParam, showDocSource, showSource, whenRunningInTest, withStoryCard };
|
package/esm/index.js
CHANGED
|
@@ -190,7 +190,7 @@ const channel = addons.getChannel();
|
|
|
190
190
|
* @param options.placement - Where to show the source code relative to the story.
|
|
191
191
|
* @returns A decorator function that shows the source code of a story above or below the rendered story
|
|
192
192
|
*/
|
|
193
|
-
function showSource({ className, placement, showOriginalSource, source, ...options } = {}) {
|
|
193
|
+
function showSource({ className, placement, showOriginalSource, source, language, ...options } = {}) {
|
|
194
194
|
if (isRunningInTest()) return (Story) => /* @__PURE__ */ jsx(Story, {});
|
|
195
195
|
return (Story, { parameters: { docs, darkMode } }) => {
|
|
196
196
|
const storedItem = window.localStorage.getItem("sb-addon-themes-3");
|
|
@@ -202,13 +202,13 @@ function showSource({ className, placement, showOriginalSource, source, ...optio
|
|
|
202
202
|
}, []);
|
|
203
203
|
const originalSource = showOriginalSource ? docs?.source?.originalSource : docs?.source?.code ?? docs?.source?.originalSource;
|
|
204
204
|
const code = typeof source === "function" ? source(originalSource) : source ?? originalSource;
|
|
205
|
-
const
|
|
205
|
+
const lang = language ?? (code === docs?.source?.originalSource ? void 0 : docs?.source?.language);
|
|
206
206
|
const isOriginalSource = code === docs?.source?.originalSource;
|
|
207
207
|
const sourceContent = useMemo(() => /* @__PURE__ */ jsx(SyntaxHighlighter, {
|
|
208
208
|
"data-testid": "source-content",
|
|
209
|
-
language,
|
|
209
|
+
language: lang,
|
|
210
210
|
children: code
|
|
211
|
-
}), [code,
|
|
211
|
+
}), [code, lang]);
|
|
212
212
|
const showBefore = (placement ?? "before") === "before";
|
|
213
213
|
const sourceCardClassName = useCallback((state) => {
|
|
214
214
|
const modifiedState = {
|
|
@@ -371,6 +371,140 @@ function withStoryCard({ title, status, appearance, content: contentProp, classN
|
|
|
371
371
|
};
|
|
372
372
|
}
|
|
373
373
|
//#endregion
|
|
374
|
+
//#region src/parameters/create_story_sort.ts
|
|
375
|
+
function getPositionInOrder(name, order) {
|
|
376
|
+
const wildcardIdx = order.indexOf("*");
|
|
377
|
+
for (let i = 0; i < order.length; i++) {
|
|
378
|
+
const entry = order[i];
|
|
379
|
+
if ((Array.isArray(entry) ? entry[0] : entry) === name) return i;
|
|
380
|
+
}
|
|
381
|
+
if (wildcardIdx !== -1) return wildcardIdx;
|
|
382
|
+
return order.length;
|
|
383
|
+
}
|
|
384
|
+
function getChildOrder(name, order) {
|
|
385
|
+
for (const entry of order) if (Array.isArray(entry) && entry[0] === name) return entry[1];
|
|
386
|
+
}
|
|
387
|
+
function isWildcardPosition(name, order) {
|
|
388
|
+
for (const entry of order) if ((Array.isArray(entry) ? entry[0] : entry) === name) return false;
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
function compareByKindOrder(aTitle, bTitle, kindOrder) {
|
|
392
|
+
const aParts = aTitle.split("/");
|
|
393
|
+
const bParts = bTitle.split("/");
|
|
394
|
+
let currentOrder = kindOrder;
|
|
395
|
+
const maxDepth = Math.max(aParts.length, bParts.length);
|
|
396
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
397
|
+
const aPart = aParts[depth];
|
|
398
|
+
const bPart = bParts[depth];
|
|
399
|
+
if (aPart === void 0 && bPart !== void 0) return -1;
|
|
400
|
+
if (aPart !== void 0 && bPart === void 0) return 1;
|
|
401
|
+
if (aPart === bPart) {
|
|
402
|
+
const childOrder = getChildOrder(aPart, currentOrder);
|
|
403
|
+
if (childOrder) currentOrder = childOrder;
|
|
404
|
+
else continue;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const aPos = getPositionInOrder(aPart, currentOrder);
|
|
408
|
+
const bPos = getPositionInOrder(bPart, currentOrder);
|
|
409
|
+
if (aPos !== bPos) return aPos - bPos;
|
|
410
|
+
const aIsWildcard = isWildcardPosition(aPart, currentOrder);
|
|
411
|
+
const bIsWildcard = isWildcardPosition(bPart, currentOrder);
|
|
412
|
+
if (aIsWildcard && bIsWildcard) return aPart.localeCompare(bPart, void 0, { numeric: true });
|
|
413
|
+
return aPart.localeCompare(bPart, void 0, { numeric: true });
|
|
414
|
+
}
|
|
415
|
+
return 0;
|
|
416
|
+
}
|
|
417
|
+
function getTagPriority(tags, tagOrder) {
|
|
418
|
+
if (!tags) return getWildcardOrEnd(tagOrder);
|
|
419
|
+
let bestPriority = -1;
|
|
420
|
+
for (const tag of tags) {
|
|
421
|
+
const pos = getExplicitPosition(tag, tagOrder);
|
|
422
|
+
if (pos !== -1 && (bestPriority === -1 || pos < bestPriority)) bestPriority = pos;
|
|
423
|
+
}
|
|
424
|
+
if (bestPriority === -1) return getWildcardOrEnd(tagOrder);
|
|
425
|
+
return bestPriority;
|
|
426
|
+
}
|
|
427
|
+
function getExplicitPosition(name, order) {
|
|
428
|
+
for (let i = 0; i < order.length; i++) {
|
|
429
|
+
const entry = order[i];
|
|
430
|
+
if ((Array.isArray(entry) ? entry[0] : entry) === name) return i;
|
|
431
|
+
}
|
|
432
|
+
return -1;
|
|
433
|
+
}
|
|
434
|
+
function getWildcardOrEnd(order) {
|
|
435
|
+
const wildcardIdx = order.indexOf("*");
|
|
436
|
+
return wildcardIdx !== -1 ? wildcardIdx : order.length;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Creates a story sort comparator for Storybook that sorts by kind (title hierarchy)
|
|
440
|
+
* and then by tag category within the same component.
|
|
441
|
+
*
|
|
442
|
+
* In Storybook 10+, `storySort` must be defined as an inline function.
|
|
443
|
+
* Assign the result to a variable and delegate from the inline function:
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```ts
|
|
447
|
+
* import { createStorySort } from '@repobuddy/storybook'
|
|
448
|
+
*
|
|
449
|
+
* const compare = createStorySort({
|
|
450
|
+
* order: ['Overview', 'components', 'hooks', '*', 'widgets'],
|
|
451
|
+
* tag: ['playground', 'use-case', 'example', 'spec', 'props', '*', 'edge-case', 'unit']
|
|
452
|
+
* })
|
|
453
|
+
*
|
|
454
|
+
* export default {
|
|
455
|
+
* parameters: {
|
|
456
|
+
* options: {
|
|
457
|
+
* storySort(a, b) {
|
|
458
|
+
* return compare(a, b)
|
|
459
|
+
* }
|
|
460
|
+
* }
|
|
461
|
+
* }
|
|
462
|
+
* }
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
function createStorySort(options = {}) {
|
|
466
|
+
return (a, b) => compareStories(a, b, options);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Compares two stories for sorting by kind (title hierarchy)
|
|
470
|
+
* and then by tag category within the same component.
|
|
471
|
+
*
|
|
472
|
+
* Use this directly inside an inline `storySort` function in Storybook 10+,
|
|
473
|
+
* which requires `storySort` to be defined as an inline function.
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```ts
|
|
477
|
+
* import { compareStories, type OrderList } from '@repobuddy/storybook'
|
|
478
|
+
*
|
|
479
|
+
* const order: OrderList = ['Overview', 'components', '*']
|
|
480
|
+
* const tag: OrderList = ['playground', 'use-case', '*', 'unit']
|
|
481
|
+
*
|
|
482
|
+
* export default {
|
|
483
|
+
* parameters: {
|
|
484
|
+
* options: {
|
|
485
|
+
* storySort(a, b) {
|
|
486
|
+
* return compareStories(a, b, { order, tag })
|
|
487
|
+
* }
|
|
488
|
+
* }
|
|
489
|
+
* }
|
|
490
|
+
* }
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
function compareStories(a, b, options = {}) {
|
|
494
|
+
const { order: kindOrder, tag: tagOrder } = options;
|
|
495
|
+
if (kindOrder && a.title !== b.title) {
|
|
496
|
+
const kindResult = compareByKindOrder(a.title, b.title, kindOrder);
|
|
497
|
+
if (kindResult !== 0) return kindResult;
|
|
498
|
+
}
|
|
499
|
+
if (a.title !== b.title) return a.title.localeCompare(b.title, void 0, { numeric: true });
|
|
500
|
+
if (tagOrder) {
|
|
501
|
+
const aPriority = getTagPriority(a.tags, tagOrder);
|
|
502
|
+
const bPriority = getTagPriority(b.tags, tagOrder);
|
|
503
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
504
|
+
}
|
|
505
|
+
return a.name.localeCompare(b.name, void 0, { numeric: true });
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
374
508
|
//#region src/parameters/define_actions_param.ts
|
|
375
509
|
/**
|
|
376
510
|
* Defines actions parameters for Storybook stories.
|
|
@@ -589,4 +723,4 @@ function whenRunningInTest(decoratorOrHandler) {
|
|
|
589
723
|
};
|
|
590
724
|
}
|
|
591
725
|
//#endregion
|
|
592
|
-
export { ShowHtml, StoryCard, defineActionsParam, defineBackgroundsParam, defineDocsParam, defineLayoutParam, defineParameters, defineStoryCardParam, defineTestParam, defineViewportParam, showDocSource, showSource, whenRunningInTest, withStoryCard };
|
|
726
|
+
export { ShowHtml, StoryCard, createStorySort, defineActionsParam, defineBackgroundsParam, defineDocsParam, defineLayoutParam, defineParameters, defineStoryCardParam, defineTestParam, defineViewportParam, showDocSource, showSource, whenRunningInTest, withStoryCard };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repobuddy/storybook",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.30.0",
|
|
4
4
|
"description": "Storybook repo buddy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"storybook",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"#repobuddy/storybook": "./src/index.ts",
|
|
28
28
|
"#repobuddy/storybook/manager": "./src/manager/index.ts",
|
|
29
29
|
"#repobuddy/storybook/storybook-addon-tag-badges": "./src/storybook-addon-tag-badges/index.ts",
|
|
30
|
+
"#storybook-addon-tag-badges": "./src/storybook-addon-tag-badges/index.ts",
|
|
30
31
|
"#repobuddy/storybook/storybook-dark-mode": "./src/storybook-dark-mode/index.ts"
|
|
31
32
|
},
|
|
32
33
|
"exports": {
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
"@tailwindcss/cli": "^4.1.17",
|
|
91
92
|
"@tailwindcss/vite": "^4.1.17",
|
|
92
93
|
"@vitest/browser": "^4.0.16",
|
|
93
|
-
"@vitest/browser-playwright": "^4.
|
|
94
|
+
"@vitest/browser-playwright": "^4.1.4",
|
|
94
95
|
"@vitest/coverage-v8": "^4.0.16",
|
|
95
96
|
"dedent": "^1.7.0",
|
|
96
97
|
"npm-run-all2": "^8.0.4",
|
|
@@ -100,8 +101,8 @@
|
|
|
100
101
|
"storybook": "^10.3.5",
|
|
101
102
|
"storybook-addon-code-editor": "^6.1.3",
|
|
102
103
|
"storybook-addon-tag-badges": "^3.1.0",
|
|
103
|
-
"storybook-addon-vis": "^
|
|
104
|
-
"tsdown": "^0.
|
|
104
|
+
"storybook-addon-vis": "^4.0.0",
|
|
105
|
+
"tsdown": "^0.22.0",
|
|
105
106
|
"vite": "^8.0.8",
|
|
106
107
|
"vitest": "^4.0.16"
|
|
107
108
|
},
|
|
@@ -22,6 +22,7 @@ export type ShowSourceOptions = Pick<StoryCardProps, 'className' | 'data-testid'
|
|
|
22
22
|
source?: ((source: string | undefined) => string) | string | undefined
|
|
23
23
|
showOriginalSource?: boolean | undefined
|
|
24
24
|
placement?: 'before' | 'after' | undefined
|
|
25
|
+
language?: 'json' | 'md' | 'html' | 'css' | 'js' | 'ts' | (string & {}) | undefined
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -40,6 +41,7 @@ export function showSource<TRenderer extends Renderer = Renderer, TArgs = Args>(
|
|
|
40
41
|
placement,
|
|
41
42
|
showOriginalSource,
|
|
42
43
|
source,
|
|
44
|
+
language,
|
|
43
45
|
...options
|
|
44
46
|
}: ShowSourceOptions = {}): DecoratorFunction<TRenderer, TArgs> {
|
|
45
47
|
if (isRunningInTest()) {
|
|
@@ -64,17 +66,17 @@ export function showSource<TRenderer extends Renderer = Renderer, TArgs = Args>(
|
|
|
64
66
|
|
|
65
67
|
const code = typeof source === 'function' ? source(originalSource) : (source ?? originalSource)
|
|
66
68
|
|
|
67
|
-
const
|
|
69
|
+
const lang = language ?? (code === docs?.source?.originalSource ? undefined : docs?.source?.language)
|
|
68
70
|
|
|
69
71
|
const isOriginalSource = code === docs?.source?.originalSource
|
|
70
72
|
|
|
71
73
|
const sourceContent = useMemo(
|
|
72
74
|
() => (
|
|
73
|
-
<SyntaxHighlighter data-testid="source-content" language={
|
|
75
|
+
<SyntaxHighlighter data-testid="source-content" language={lang}>
|
|
74
76
|
{code}
|
|
75
77
|
</SyntaxHighlighter>
|
|
76
78
|
),
|
|
77
|
-
[code,
|
|
79
|
+
[code, lang]
|
|
78
80
|
)
|
|
79
81
|
|
|
80
82
|
const showBefore = (placement ?? 'before') === 'before'
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './components/story_card.tsx'
|
|
|
5
5
|
export * from './decorators/show_doc_source.tsx'
|
|
6
6
|
export * from './decorators/show_source.tsx'
|
|
7
7
|
export * from './decorators/with_story_card.tsx'
|
|
8
|
+
export * from './parameters/create_story_sort.ts'
|
|
8
9
|
export * from './parameters/define_actions_param.ts'
|
|
9
10
|
export * from './parameters/define_backgrounds_param.ts'
|
|
10
11
|
export * from './parameters/define_docs_param.ts'
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
type Story = {
|
|
2
|
+
id: string
|
|
3
|
+
importPath: string
|
|
4
|
+
name: string
|
|
5
|
+
title: string
|
|
6
|
+
tags?: string[] | undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type StorySortFn = (a: Story, b: Story) => number
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A nested ordering list. Each entry is either:
|
|
13
|
+
* - A string (matches a title segment or tag name; `'*'` is the wildcard)
|
|
14
|
+
* - A tuple `[name, children]` for nested sub-ordering (order only)
|
|
15
|
+
*/
|
|
16
|
+
export type OrderList = Array<string | [string, OrderList]>
|
|
17
|
+
|
|
18
|
+
export interface StorySortOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Ordered list of title path segments (root-level categories).
|
|
21
|
+
* Supports nesting via tuples and `'*'` wildcard.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* order: ['Overview', 'components', ['Pages', ['Home', 'Login']], '*', 'WIP']
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
order?: OrderList | undefined
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ordered list of story category tags.
|
|
32
|
+
* Stories sharing the same title are sorted by their first matching tag.
|
|
33
|
+
* `'*'` marks where stories with unlisted tags sort.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* tag: ['playground', 'use-case', 'spec', 'props', '*', 'unit', 'integration']
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
tag?: OrderList | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getPositionInOrder(name: string, order: OrderList): number {
|
|
44
|
+
const wildcardIdx = order.indexOf('*')
|
|
45
|
+
for (let i = 0; i < order.length; i++) {
|
|
46
|
+
const entry = order[i]
|
|
47
|
+
const entryName = Array.isArray(entry) ? entry[0] : entry
|
|
48
|
+
if (entryName === name) return i
|
|
49
|
+
}
|
|
50
|
+
if (wildcardIdx !== -1) return wildcardIdx
|
|
51
|
+
return order.length
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getChildOrder(name: string, order: OrderList): OrderList | undefined {
|
|
55
|
+
for (const entry of order) {
|
|
56
|
+
if (Array.isArray(entry) && entry[0] === name) {
|
|
57
|
+
return entry[1]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isWildcardPosition(name: string, order: OrderList): boolean {
|
|
64
|
+
for (const entry of order) {
|
|
65
|
+
const entryName = Array.isArray(entry) ? entry[0] : entry
|
|
66
|
+
if (entryName === name) return false
|
|
67
|
+
}
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compareByKindOrder(aTitle: string, bTitle: string, kindOrder: OrderList): number {
|
|
72
|
+
const aParts = aTitle.split('/')
|
|
73
|
+
const bParts = bTitle.split('/')
|
|
74
|
+
let currentOrder = kindOrder
|
|
75
|
+
|
|
76
|
+
const maxDepth = Math.max(aParts.length, bParts.length)
|
|
77
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
78
|
+
const aPart = aParts[depth]
|
|
79
|
+
const bPart = bParts[depth]
|
|
80
|
+
|
|
81
|
+
if (aPart === undefined && bPart !== undefined) return -1
|
|
82
|
+
if (aPart !== undefined && bPart === undefined) return 1
|
|
83
|
+
if (aPart === bPart) {
|
|
84
|
+
const childOrder = getChildOrder(aPart!, currentOrder)
|
|
85
|
+
if (childOrder) {
|
|
86
|
+
currentOrder = childOrder
|
|
87
|
+
} else {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const aPos = getPositionInOrder(aPart!, currentOrder)
|
|
94
|
+
const bPos = getPositionInOrder(bPart!, currentOrder)
|
|
95
|
+
|
|
96
|
+
if (aPos !== bPos) return aPos - bPos
|
|
97
|
+
|
|
98
|
+
const aIsWildcard = isWildcardPosition(aPart!, currentOrder)
|
|
99
|
+
const bIsWildcard = isWildcardPosition(bPart!, currentOrder)
|
|
100
|
+
if (aIsWildcard && bIsWildcard) {
|
|
101
|
+
return aPart!.localeCompare(bPart!, undefined, { numeric: true })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return aPart!.localeCompare(bPart!, undefined, { numeric: true })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getTagPriority(tags: string[] | undefined, tagOrder: OrderList): number {
|
|
111
|
+
if (!tags) return getWildcardOrEnd(tagOrder)
|
|
112
|
+
let bestPriority = -1
|
|
113
|
+
for (const tag of tags) {
|
|
114
|
+
const pos = getExplicitPosition(tag, tagOrder)
|
|
115
|
+
if (pos !== -1 && (bestPriority === -1 || pos < bestPriority)) {
|
|
116
|
+
bestPriority = pos
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (bestPriority === -1) return getWildcardOrEnd(tagOrder)
|
|
120
|
+
return bestPriority
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getExplicitPosition(name: string, order: OrderList): number {
|
|
124
|
+
for (let i = 0; i < order.length; i++) {
|
|
125
|
+
const entry = order[i]
|
|
126
|
+
const entryName = Array.isArray(entry) ? entry[0] : entry
|
|
127
|
+
if (entryName === name) return i
|
|
128
|
+
}
|
|
129
|
+
return -1
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getWildcardOrEnd(order: OrderList): number {
|
|
133
|
+
const wildcardIdx = order.indexOf('*')
|
|
134
|
+
return wildcardIdx !== -1 ? wildcardIdx : order.length
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Creates a story sort comparator for Storybook that sorts by kind (title hierarchy)
|
|
139
|
+
* and then by tag category within the same component.
|
|
140
|
+
*
|
|
141
|
+
* In Storybook 10+, `storySort` must be defined as an inline function.
|
|
142
|
+
* Assign the result to a variable and delegate from the inline function:
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* import { createStorySort } from '@repobuddy/storybook'
|
|
147
|
+
*
|
|
148
|
+
* const compare = createStorySort({
|
|
149
|
+
* order: ['Overview', 'components', 'hooks', '*', 'widgets'],
|
|
150
|
+
* tag: ['playground', 'use-case', 'example', 'spec', 'props', '*', 'edge-case', 'unit']
|
|
151
|
+
* })
|
|
152
|
+
*
|
|
153
|
+
* export default {
|
|
154
|
+
* parameters: {
|
|
155
|
+
* options: {
|
|
156
|
+
* storySort(a, b) {
|
|
157
|
+
* return compare(a, b)
|
|
158
|
+
* }
|
|
159
|
+
* }
|
|
160
|
+
* }
|
|
161
|
+
* }
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function createStorySort(options: StorySortOptions = {}): StorySortFn {
|
|
165
|
+
return (a, b) => compareStories(a, b, options)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compares two stories for sorting by kind (title hierarchy)
|
|
170
|
+
* and then by tag category within the same component.
|
|
171
|
+
*
|
|
172
|
+
* Use this directly inside an inline `storySort` function in Storybook 10+,
|
|
173
|
+
* which requires `storySort` to be defined as an inline function.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* import { compareStories, type OrderList } from '@repobuddy/storybook'
|
|
178
|
+
*
|
|
179
|
+
* const order: OrderList = ['Overview', 'components', '*']
|
|
180
|
+
* const tag: OrderList = ['playground', 'use-case', '*', 'unit']
|
|
181
|
+
*
|
|
182
|
+
* export default {
|
|
183
|
+
* parameters: {
|
|
184
|
+
* options: {
|
|
185
|
+
* storySort(a, b) {
|
|
186
|
+
* return compareStories(a, b, { order, tag })
|
|
187
|
+
* }
|
|
188
|
+
* }
|
|
189
|
+
* }
|
|
190
|
+
* }
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
function compareStories(
|
|
194
|
+
a: { title: string; name: string; tags?: string[] },
|
|
195
|
+
b: { title: string; name: string; tags?: string[] },
|
|
196
|
+
options: StorySortOptions = {}
|
|
197
|
+
): number {
|
|
198
|
+
const { order: kindOrder, tag: tagOrder } = options
|
|
199
|
+
|
|
200
|
+
if (kindOrder && a.title !== b.title) {
|
|
201
|
+
const kindResult = compareByKindOrder(a.title, b.title, kindOrder)
|
|
202
|
+
if (kindResult !== 0) return kindResult
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (a.title !== b.title) {
|
|
206
|
+
return a.title.localeCompare(b.title, undefined, { numeric: true })
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (tagOrder) {
|
|
210
|
+
const aPriority = getTagPriority(a.tags, tagOrder)
|
|
211
|
+
const bPriority = getTagPriority(b.tags, tagOrder)
|
|
212
|
+
if (aPriority !== bPriority) return aPriority - bPriority
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return a.name.localeCompare(b.name, undefined, { numeric: true })
|
|
216
|
+
}
|
package/styles.css
CHANGED