From 216f3d658e8a5e4e81bad0db8cd8dbc0c5e7c20a Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Fri, 16 Feb 2024 17:15:02 +0800 Subject: [PATCH 01/53] UI WIP --- .../core-server/src/presets/common-manager.ts | 49 ++++++++++++++----- .../src/components/sidebar/Sidebar.tsx | 19 ++++++- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/code/lib/core-server/src/presets/common-manager.ts b/code/lib/core-server/src/presets/common-manager.ts index 0564f8e00b92..f12bd6c4b8ed 100644 --- a/code/lib/core-server/src/presets/common-manager.ts +++ b/code/lib/core-server/src/presets/common-manager.ts @@ -1,24 +1,49 @@ import { addons } from '@storybook/manager-api'; import { global } from '@storybook/global'; +import type { Tag } from '@storybook/types'; +const TAG_FILTERS = 'tag-filters'; const STATIC_FILTER = 'static-filter'; +const UI_FILTER = 'ui-filter'; -addons.register(STATIC_FILTER, (api) => { +const parseTags = (tags?: string) => { + if (!tags) return undefined; + return tags.split(',').reduce((acc, tag) => { + tag.trim(); + acc[tag] = true; + return acc; + }, {} as Record); +}; + +addons.register(TAG_FILTERS, (api) => { // FIXME: this ensures the filter is applied after the first render // to avoid a strange race condition in Webkit only. - const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce( - (acc, entry) => { - const [tag, option] = entry; - if ((option as any).excludeFromSidebar) { - acc[tag] = true; - } - return acc; - }, - {} as Record - ); + const staticExludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce((acc, entry) => { + const [tag, option] = entry; + if ((option as any).excludeFromSidebar) { + acc[tag] = true; + } + return acc; + }, {} as Record); api.experimental_setFilter(STATIC_FILTER, (item) => { const tags = item.tags || []; - return tags.filter((tag) => excludeTags[tag]).length === 0; + return tags.filter((tag) => staticExludeTags[tag]).length === 0; + }); + + api.experimental_setFilter(UI_FILTER, (item) => { + const tags = item.tags || []; + const { queryParams } = api.getUrlState(); + const tagToInclude = parseTags(queryParams.tagsFilter); + + if (!tagToInclude) return true; + + let include = true; + include = tags.some((tag) => tagToInclude[tag)); + + if (excludeTags) { + include = !tags.some((tag) => excludeTags.includes(tag)); + } + return include; }); }); diff --git a/code/ui/manager/src/components/sidebar/Sidebar.tsx b/code/ui/manager/src/components/sidebar/Sidebar.tsx index 3ea23f6b48a5..248e8d4adfe0 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.tsx @@ -1,7 +1,8 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { styled } from '@storybook/theming'; -import { ScrollArea, Spaced } from '@storybook/components'; +import { IconButton, ScrollArea, Spaced } from '@storybook/components'; +import { FilterIcon } from '@storybook/icons'; import type { State } from '@storybook/manager-api'; import type { @@ -129,6 +130,17 @@ export const Sidebar = React.memo(function Sidebar({ const dataset = useCombination({ index, indexError, previewInitialized, status }, refs); const isLoading = !index && !indexError; const lastViewedProps = useLastViewed(selected); + const [tagsActive, setTagsActive] = useState(false); + const toggleTags = () => { + setTagsActive(!tagsActive); + const url = new URL(window.location.href); + if (tagsActive) { + url.searchParams.delete('includeTags'); + } else { + url.searchParams.set('includeTags', 'bar'); + } + window.history.pushState({}, '', url); + }; return ( @@ -174,6 +186,9 @@ export const Sidebar = React.memo(function Sidebar({ )} + + + {isLoading ? null : ( From a5f8eb105089a7eeae1953b09b6228f486c43bb8 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Fri, 14 Jun 2024 11:56:26 +0800 Subject: [PATCH 02/53] Tag UI: state management --- .../src/components/sidebar/Sidebar.tsx | 25 ++------ .../src/components/sidebar/TagsFilter.tsx | 60 +++++++++++++++++++ 2 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 code/ui/manager/src/components/sidebar/TagsFilter.tsx diff --git a/code/ui/manager/src/components/sidebar/Sidebar.tsx b/code/ui/manager/src/components/sidebar/Sidebar.tsx index bcb0271864a1..6f4c6eee2b8c 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.tsx @@ -1,9 +1,8 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { styled } from '@storybook/theming'; -import { IconButton, ScrollArea, Spaced } from '@storybook/components'; -import { FilterIcon } from '@storybook/icons'; -import type { State } from '@storybook/manager-api'; +import { ScrollArea, Spaced } from '@storybook/components'; +import { useStorybookApi, type State } from '@storybook/manager-api'; import type { Addon_SidebarBottomType, @@ -14,7 +13,7 @@ import type { HeadingProps } from './Heading'; import { Heading } from './Heading'; import { Explorer } from './Explorer'; - +import { TagsFilter } from './TagsFilter'; import { Search } from './Search'; import { SearchResults } from './SearchResults'; @@ -138,17 +137,7 @@ export const Sidebar = React.memo(function Sidebar({ const dataset = useCombination(index, indexError, previewInitialized, status, refs); const isLoading = !index && !indexError; const lastViewedProps = useLastViewed(selected); - const [tagsActive, setTagsActive] = useState(false); - const toggleTags = () => { - setTagsActive(!tagsActive); - const url = new URL(window.location.href); - if (tagsActive) { - url.searchParams.delete('includeTags'); - } else { - url.searchParams.set('includeTags', 'bar'); - } - window.history.pushState({}, '', url); - }; + const api = useStorybookApi(); return ( @@ -163,6 +152,7 @@ export const Sidebar = React.memo(function Sidebar({ isLoading={isLoading} onMenuClick={onMenuClick} /> + )} - - - {isLoading ? null : ( diff --git a/code/ui/manager/src/components/sidebar/TagsFilter.tsx b/code/ui/manager/src/components/sidebar/TagsFilter.tsx new file mode 100644 index 000000000000..e8ec89c0252a --- /dev/null +++ b/code/ui/manager/src/components/sidebar/TagsFilter.tsx @@ -0,0 +1,60 @@ +import React, { useState, useEffect } from 'react'; +import type { API } from '@storybook/manager-api'; +import type { Tag, API_IndexHash } from '@storybook/types'; +import { IconButton } from '@storybook/components'; +import { FilterIcon } from '@storybook/icons'; + +interface TagsFilterProps { + api: API; + index: API_IndexHash; +} + +const UI_FILTER = 'ui-filter'; + +export const TagsFilter = ({ api }: TagsFilterProps) => { + const [includeTags, setIncludeTags] = useState([]); + const [excludeTags, setExcludeTags] = useState([]); + const tagsActive = includeTags.length + excludeTags.length > 0; + + const updateTag = (tag: Tag, selected: boolean, include: boolean) => { + const [filter, setFilter, queryParam] = include + ? [includeTags, setIncludeTags, 'includeTags'] + : [excludeTags, setExcludeTags, 'excludeTags']; + + // no change needed for state/url if the tag is already in the correct state + if ((selected && filter.includes(tag)) || (!selected && !filter.includes(tag))) return; + + // update state + const newFilter = selected ? [...filter, tag] : filter.filter((t) => t !== tag); + setFilter(newFilter); + + // update URL + const url = new URL(window.location.href); + if (newFilter.length === 0) { + url.searchParams.delete(queryParam); + } else { + url.searchParams.set(queryParam, newFilter.join(',')); + } + window.history.pushState({}, '', url); + }; + + const toggleTags = () => { + // updateTag('bar', !includeTags.includes('bar'), true); + updateTag('bar', !excludeTags.includes('bar'), false); + }; + + useEffect(() => { + api.experimental_setFilter(UI_FILTER, (item) => { + const tags = item.tags ?? []; + if (excludeTags.some((tag) => tags.includes(tag))) return false; + if (!includeTags.every((tag) => tags.includes(tag))) return false; + return true; + }); + }, [api, includeTags, excludeTags]); + + return ( + + + + ); +}; From be48b2ba27a94b8b52a50c240daee576e9ac69c3 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sun, 16 Jun 2024 20:00:33 +0800 Subject: [PATCH 03/53] TagsFilter: Initial implementation and stories --- .../src/components/sidebar/Sidebar.tsx | 16 ++- .../components/sidebar/TagsFilter.stories.tsx | 44 ++++++ .../src/components/sidebar/TagsFilter.tsx | 110 ++++++++------ .../sidebar/TagsFilterPanel.stories.tsx | 52 +++++++ .../components/sidebar/TagsFilterPanel.tsx | 135 ++++++++++++++++++ 5 files changed, 312 insertions(+), 45 deletions(-) create mode 100644 code/ui/manager/src/components/sidebar/TagsFilter.stories.tsx create mode 100644 code/ui/manager/src/components/sidebar/TagsFilterPanel.stories.tsx create mode 100644 code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx diff --git a/code/ui/manager/src/components/sidebar/Sidebar.tsx b/code/ui/manager/src/components/sidebar/Sidebar.tsx index 6f4c6eee2b8c..2c98f1a5c695 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.tsx @@ -152,7 +152,21 @@ export const Sidebar = React.memo(function Sidebar({ isLoading={isLoading} onMenuClick={onMenuClick} /> - + { + const url = new URL(window.location.href); + Object.entries(params).forEach(([key, value]) => { + if (value) { + url.searchParams.set(key, value); + } else { + url.searchParams.delete(key); + } + }); + window.history.pushState({}, '', url); + }} + /> ; + +export default meta; + +type Story = StoryObj; + +export const Closed: Story = { + args: { + api: { + experimental_setFilter: fn(), + } as any, + index: { + story1: { type: 'story', tags: ['A', 'B', 'C', 'dev'] } as any, + }, + updateQueryParams: fn(), + }, +}; + +export const ClosedWithSelection: Story = { + args: { + ...Closed.args, + initialSelectedTags: ['A', 'B'], + }, +}; + +export const Open: Story = { + ...Closed, + play: async ({ canvasElement }) => { + const button = await findByRole(canvasElement, 'button'); + await button.click(); + }, +}; + +export const OpenWithSelection: Story = { + ...ClosedWithSelection, + play: Open.play, +}; diff --git a/code/ui/manager/src/components/sidebar/TagsFilter.tsx b/code/ui/manager/src/components/sidebar/TagsFilter.tsx index e8ec89c0252a..1945459da7a3 100644 --- a/code/ui/manager/src/components/sidebar/TagsFilter.tsx +++ b/code/ui/manager/src/components/sidebar/TagsFilter.tsx @@ -1,60 +1,82 @@ import React, { useState, useEffect } from 'react'; +import { IconButton, WithTooltip } from '@storybook/components'; +import { FilterIcon } from '@storybook/icons'; import type { API } from '@storybook/manager-api'; import type { Tag, API_IndexHash } from '@storybook/types'; -import { IconButton } from '@storybook/components'; -import { FilterIcon } from '@storybook/icons'; +import { TagsFilterPanel } from './TagsFilterPanel'; -interface TagsFilterProps { +const TAGS_FILTER = 'tags-filter'; + +export interface TagsFilterProps { api: API; index: API_IndexHash; + updateQueryParams: (params: Record) => void; + initialSelectedTags?: Tag[]; } -const UI_FILTER = 'ui-filter'; - -export const TagsFilter = ({ api }: TagsFilterProps) => { - const [includeTags, setIncludeTags] = useState([]); - const [excludeTags, setExcludeTags] = useState([]); - const tagsActive = includeTags.length + excludeTags.length > 0; - - const updateTag = (tag: Tag, selected: boolean, include: boolean) => { - const [filter, setFilter, queryParam] = include - ? [includeTags, setIncludeTags, 'includeTags'] - : [excludeTags, setExcludeTags, 'excludeTags']; - - // no change needed for state/url if the tag is already in the correct state - if ((selected && filter.includes(tag)) || (!selected && !filter.includes(tag))) return; - - // update state - const newFilter = selected ? [...filter, tag] : filter.filter((t) => t !== tag); - setFilter(newFilter); - - // update URL - const url = new URL(window.location.href); - if (newFilter.length === 0) { - url.searchParams.delete(queryParam); - } else { - url.searchParams.set(queryParam, newFilter.join(',')); - } - window.history.pushState({}, '', url); - }; - - const toggleTags = () => { - // updateTag('bar', !includeTags.includes('bar'), true); - updateTag('bar', !excludeTags.includes('bar'), false); - }; +export const TagsFilter = ({ + api, + index, + updateQueryParams, + initialSelectedTags = [], +}: TagsFilterProps) => { + const [selectedTags, setSelectedTags] = useState(initialSelectedTags); + const [exclude, setExclude] = useState(false); + const [expanded, setExpanded] = useState(false); + const tagsActive = selectedTags.length > 0; useEffect(() => { - api.experimental_setFilter(UI_FILTER, (item) => { + api.experimental_setFilter(TAGS_FILTER, (item) => { const tags = item.tags ?? []; - if (excludeTags.some((tag) => tags.includes(tag))) return false; - if (!includeTags.every((tag) => tags.includes(tag))) return false; - return true; + return exclude + ? !selectedTags.some((tag) => tags.includes(tag)) + : selectedTags.every((tag) => tags.includes(tag)); }); - }, [api, includeTags, excludeTags]); + + const tagsParam = selectedTags.join(','); + const [includeTags, excludeTags] = exclude ? [null, tagsParam] : [tagsParam, null]; + updateQueryParams({ includeTags, excludeTags }); + }, [api, selectedTags, exclude, updateQueryParams]); + + const allTags = Object.values(index).reduce((acc, entry) => { + if (entry.type === 'story') { + entry.tags.forEach((tag: Tag) => acc.add(tag)); + } + return acc; + }, new Set()); return ( - - - + ( + { + if (selectedTags.includes(tag)) { + setSelectedTags(selectedTags.filter((t) => t !== tag)); + } else { + setSelectedTags([...selectedTags, tag]); + } + }} + toggleExclude={() => setExclude(!exclude)} + /> + )} + > + { + event.preventDefault(); + setExpanded(!expanded); + }} + > + + + ); }; diff --git a/code/ui/manager/src/components/sidebar/TagsFilterPanel.stories.tsx b/code/ui/manager/src/components/sidebar/TagsFilterPanel.stories.tsx new file mode 100644 index 000000000000..b97093b2d0e1 --- /dev/null +++ b/code/ui/manager/src/components/sidebar/TagsFilterPanel.stories.tsx @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { TagsFilterPanel } from './TagsFilterPanel'; + +const meta = { + component: TagsFilterPanel, + args: { + exclude: false, + toggleTag: fn(), + toggleExclude: fn(), + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Empty: Story = { + args: { + allTags: [], + selectedTags: [], + }, +}; + +export const Default: Story = { + args: { + allTags: ['tag1', 'tag2', 'tag3'], + selectedTags: ['tag1', 'tag3'], + }, +}; + +export const Exclude: Story = { + args: { + ...Default.args, + exclude: true, + }, +}; + +export const BuiltInTags: Story = { + args: { + allTags: [...Default.args.allTags, 'dev', 'autodocs'], + selectedTags: ['tag1', 'tag3'], + }, +}; + +export const BuiltInTagsSelected: Story = { + args: { + ...BuiltInTags.args, + selectedTags: ['tag1', 'tag3', 'autodocs'], + }, +}; diff --git a/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx b/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx new file mode 100644 index 000000000000..5ab710d928e0 --- /dev/null +++ b/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx @@ -0,0 +1,135 @@ +import type { ChangeEvent } from 'react'; +import React, { useState } from 'react'; +import { transparentize } from 'polished'; +import { styled } from '@storybook/theming'; +import { CollapseIcon } from './components/CollapseIcon'; + +const BUILT_IN_TAGS = new Set(['dev', 'autodocs', 'test', 'attached-mdx', 'unattached-mdx']); + +const CollapseButton = styled.button(({ theme }) => ({ + all: 'unset', + display: 'flex', + padding: '0px 8px', + borderRadius: 4, + transition: 'color 150ms, box-shadow 150ms', + gap: 6, + alignItems: 'center', + cursor: 'pointer', + height: 28, + + '&:hover, &:focus': { + outline: 'none', + background: transparentize(0.93, theme.color.secondary), + }, +})); + +const Text = styled.span({ + '[aria-readonly=true] &': { + opacity: 0.5, + }, +}); + +const Label = styled.label({ + lineHeight: '20px', + alignItems: 'center', + marginBottom: 8, + + '&:last-child': { + marginBottom: 0, + }, + + input: { + margin: 0, + marginRight: 6, + }, +}); + +interface TagsFilterPanelProps { + allTags: Tag[]; + selectedTags: Tag[]; + exclude: boolean; + toggleTag: (tag: Tag) => void; + toggleExclude: () => void; +} + +interface TagsListProps { + tags: Tag[]; + selectedTags: Tag[]; + toggleTag: (tag: Tag) => void; +} + +const TagsList = ({ tags, selectedTags, toggleTag }: TagsListProps) => { + return tags.map((tag) => { + const checked = selectedTags.includes(tag); + const id = `tag-${tag}`; + return ( + + ); + }); +}; + +const Wrapper = styled.div({ + label: { + display: 'flex', + }, +}); + +export const TagsFilterPanel = ({ + allTags, + selectedTags, + exclude, + toggleTag, + toggleExclude, +}: TagsFilterPanelProps) => { + const userTags = allTags.filter((tag) => !BUILT_IN_TAGS.has(tag)).toSorted(); + const builtInTags = allTags.filter((tag) => BUILT_IN_TAGS.has(tag)).toSorted(); + const [builtinsExpanded, setBuiltinsExpanded] = useState( + selectedTags.some((tag) => BUILT_IN_TAGS.has(tag)) + ); + + return ( +
+ {userTags.length === 0 ? ( + 'No tags defined' + ) : ( + + Tags {exclude ? 'does not contain' : 'contains'} + + + )} + {builtInTags.length > 0 && ( + <> + { + event.preventDefault(); + setBuiltinsExpanded(!builtinsExpanded); + }} + aria-expanded={builtinsExpanded} + > + + Built-in tags + + {builtinsExpanded ? ( + + + + ) : null} + + )} +
+ ); +}; From 7b69bae00921c5454018ed71ab99586f0a1042fe Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sun, 16 Jun 2024 20:36:59 +0800 Subject: [PATCH 04/53] Only show filter after index has bene created --- .../src/components/sidebar/Sidebar.tsx | 32 ++++++++++--------- .../components/sidebar/TagsFilter.stories.tsx | 1 + .../src/components/sidebar/TagsFilter.tsx | 2 +- .../components/sidebar/TagsFilterPanel.tsx | 9 +++++- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/code/ui/manager/src/components/sidebar/Sidebar.tsx b/code/ui/manager/src/components/sidebar/Sidebar.tsx index 2c98f1a5c695..c4c1b35db9ce 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.tsx @@ -152,21 +152,23 @@ export const Sidebar = React.memo(function Sidebar({ isLoading={isLoading} onMenuClick={onMenuClick} /> - { - const url = new URL(window.location.href); - Object.entries(params).forEach(([key, value]) => { - if (value) { - url.searchParams.set(key, value); - } else { - url.searchParams.delete(key); - } - }); - window.history.pushState({}, '', url); - }} - /> + {index && ( + { + const url = new URL(window.location.href); + Object.entries(params).forEach(([key, value]) => { + if (value) { + url.searchParams.set(key, value); + } else { + url.searchParams.delete(key); + } + }); + window.history.pushState({}, '', url); + }} + /> + )} ; export default meta; diff --git a/code/ui/manager/src/components/sidebar/TagsFilter.tsx b/code/ui/manager/src/components/sidebar/TagsFilter.tsx index 1945459da7a3..e29f12292d2d 100644 --- a/code/ui/manager/src/components/sidebar/TagsFilter.tsx +++ b/code/ui/manager/src/components/sidebar/TagsFilter.tsx @@ -47,7 +47,7 @@ export const TagsFilter = ({ return ( ( diff --git a/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx b/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx index 5ab710d928e0..4f7d2e5c4b9c 100644 --- a/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx +++ b/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx @@ -4,7 +4,14 @@ import { transparentize } from 'polished'; import { styled } from '@storybook/theming'; import { CollapseIcon } from './components/CollapseIcon'; -const BUILT_IN_TAGS = new Set(['dev', 'autodocs', 'test', 'attached-mdx', 'unattached-mdx']); +const BUILT_IN_TAGS = new Set([ + 'dev', + 'autodocs', + 'test', + 'attached-mdx', + 'unattached-mdx', + 'play-fn', +]); const CollapseButton = styled.button(({ theme }) => ({ all: 'unset', From b8af8955f6cc7b3f0e199b19a0aaf52d6424687a Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Mon, 17 Jun 2024 17:29:39 +0800 Subject: [PATCH 05/53] Add count of selected filters --- .../src/components/sidebar/TagsFilter.tsx | 49 ++++++++++++++----- .../components/sidebar/TagsFilterPanel.tsx | 2 + 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/code/ui/manager/src/components/sidebar/TagsFilter.tsx b/code/ui/manager/src/components/sidebar/TagsFilter.tsx index e29f12292d2d..d248bbad77a8 100644 --- a/code/ui/manager/src/components/sidebar/TagsFilter.tsx +++ b/code/ui/manager/src/components/sidebar/TagsFilter.tsx @@ -1,12 +1,34 @@ import React, { useState, useEffect } from 'react'; -import { IconButton, WithTooltip } from '@storybook/components'; +import { Badge, IconButton, WithTooltip } from '@storybook/components'; import { FilterIcon } from '@storybook/icons'; import type { API } from '@storybook/manager-api'; +import { styled } from '@storybook/theming'; import type { Tag, API_IndexHash } from '@storybook/types'; import { TagsFilterPanel } from './TagsFilterPanel'; const TAGS_FILTER = 'tags-filter'; +const Wrapper = styled.div({ + position: 'relative', +}); + +const Count = styled(Badge)(({ theme }) => ({ + position: 'absolute', + top: 0, + right: 0, + transform: 'translate(50%, -50%)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: 3, + height: 15, + minWidth: 15, + lineHeight: '15px', + fontSize: theme.typography.size.s1 - 1, + background: theme.color.secondary, + color: theme.color.lightest, +})); + export interface TagsFilterProps { api: API; index: API_IndexHash; @@ -66,17 +88,20 @@ export const TagsFilter = ({ /> )} > - { - event.preventDefault(); - setExpanded(!expanded); - }} - > - - + + { + event.preventDefault(); + setExpanded(!expanded); + }} + > + + + {selectedTags.length > 0 && {selectedTags.length}} + ); }; diff --git a/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx b/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx index 4f7d2e5c4b9c..0f82b8baef71 100644 --- a/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx +++ b/code/ui/manager/src/components/sidebar/TagsFilterPanel.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import { transparentize } from 'polished'; import { styled } from '@storybook/theming'; import { CollapseIcon } from './components/CollapseIcon'; +import type { Tag } from '@storybook/types'; const BUILT_IN_TAGS = new Set([ 'dev', @@ -88,6 +89,7 @@ const TagsList = ({ tags, selectedTags, toggleTag }: TagsListProps) => { }; const Wrapper = styled.div({ + padding: 10, label: { display: 'flex', }, From 22a699b673198846ab2b838d853d7bbc2e61225c Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Mon, 17 Jun 2024 17:30:21 +0800 Subject: [PATCH 06/53] Fix re-rendering bug --- .../src/components/sidebar/Sidebar.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/code/ui/manager/src/components/sidebar/Sidebar.tsx b/code/ui/manager/src/components/sidebar/Sidebar.tsx index c4c1b35db9ce..5b6513d43269 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.tsx @@ -103,6 +103,17 @@ const useCombination = ( return useMemo(() => ({ hash, entries: Object.entries(hash) }), [hash]); }; +const updateQueryParams = (params: Record) => { + const url = new URL(window.location.href); + Object.entries(params).forEach(([key, value]) => { + if (value) { + url.searchParams.set(key, value); + } else { + url.searchParams.delete(key); + } + }); + window.history.pushState({}, '', url); +}; export interface SidebarProps extends API_LoadedRefData { refs: State['refs']; status: State['status']; @@ -152,23 +163,7 @@ export const Sidebar = React.memo(function Sidebar({ isLoading={isLoading} onMenuClick={onMenuClick} /> - {index && ( - { - const url = new URL(window.location.href); - Object.entries(params).forEach(([key, value]) => { - if (value) { - url.searchParams.set(key, value); - } else { - url.searchParams.delete(key); - } - }); - window.history.pushState({}, '', url); - }} - /> - )} + {index && } Date: Mon, 17 Jun 2024 19:35:58 +0800 Subject: [PATCH 07/53] TagsFilter: Don't use filtered index to generate tags --- .../src/components/sidebar/Sidebar.tsx | 7 ++++++- .../components/sidebar/TagsFilter.stories.tsx | 7 +++++-- .../src/components/sidebar/TagsFilter.tsx | 20 +++++++++---------- .../sidebar/TagsFilterPanel.stories.tsx | 1 + code/ui/manager/src/container/Sidebar.tsx | 5 +++++ 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/code/ui/manager/src/components/sidebar/Sidebar.tsx b/code/ui/manager/src/components/sidebar/Sidebar.tsx index 5b6513d43269..5fbb56aac46b 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.tsx @@ -8,6 +8,7 @@ import type { Addon_SidebarBottomType, Addon_SidebarTopType, API_LoadedRefData, + StoryIndex, } from '@storybook/types'; import type { HeadingProps } from './Heading'; import { Heading } from './Heading'; @@ -126,12 +127,14 @@ export interface SidebarProps extends API_LoadedRefData { enableShortcuts?: boolean; onMenuClick?: HeadingProps['onMenuClick']; showCreateStoryButton?: boolean; + indexJson?: StoryIndex; } export const Sidebar = React.memo(function Sidebar({ storyId = null, refId = DEFAULT_REF_ID, index, + indexJson, indexError, status, previewInitialized, @@ -163,7 +166,9 @@ export const Sidebar = React.memo(function Sidebar({ isLoading={isLoading} onMenuClick={onMenuClick} /> - {index && } + {index && ( + + )} ({ export interface TagsFilterProps { api: API; - index: API_IndexHash; + indexJson: StoryIndex; updateQueryParams: (params: Record) => void; initialSelectedTags?: Tag[]; } export const TagsFilter = ({ api, - index, + indexJson, updateQueryParams, initialSelectedTags = [], }: TagsFilterProps) => { @@ -49,10 +49,10 @@ export const TagsFilter = ({ useEffect(() => { api.experimental_setFilter(TAGS_FILTER, (item) => { - const tags = item.tags ?? []; - return exclude - ? !selectedTags.some((tag) => tags.includes(tag)) - : selectedTags.every((tag) => tags.includes(tag)); + if (selectedTags.length === 0) return true; + + const hasSelectedTags = selectedTags.some((tag) => item.tags?.includes(tag)); + return exclude ? !hasSelectedTags : hasSelectedTags; }); const tagsParam = selectedTags.join(','); @@ -60,10 +60,8 @@ export const TagsFilter = ({ updateQueryParams({ includeTags, excludeTags }); }, [api, selectedTags, exclude, updateQueryParams]); - const allTags = Object.values(index).reduce((acc, entry) => { - if (entry.type === 'story') { - entry.tags.forEach((tag: Tag) => acc.add(tag)); - } + const allTags = Object.values(indexJson.entries).reduce((acc, entry) => { + entry.tags?.forEach((tag: Tag) => acc.add(tag)); return acc; }, new Set()); diff --git a/code/ui/manager/src/components/sidebar/TagsFilterPanel.stories.tsx b/code/ui/manager/src/components/sidebar/TagsFilterPanel.stories.tsx index b97093b2d0e1..ef0b9e47002d 100644 --- a/code/ui/manager/src/components/sidebar/TagsFilterPanel.stories.tsx +++ b/code/ui/manager/src/components/sidebar/TagsFilterPanel.stories.tsx @@ -10,6 +10,7 @@ const meta = { toggleTag: fn(), toggleExclude: fn(), }, + tags: ['hoho'], } satisfies Meta; export default meta; diff --git a/code/ui/manager/src/container/Sidebar.tsx b/code/ui/manager/src/container/Sidebar.tsx index 21d9cf09ef6c..fc30fddc1a47 100755 --- a/code/ui/manager/src/container/Sidebar.tsx +++ b/code/ui/manager/src/container/Sidebar.tsx @@ -22,6 +22,10 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) { storyId, refId, layout: { showToolbar }, + // FIXME: This is the actual `index.json` index where the `index` below + // is actually the stories hash. We should fix this up and make it consistent. + // eslint-disable-next-line @typescript-eslint/naming-convention + internal_index, index, status, indexError, @@ -52,6 +56,7 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) { return { title: name, url, + indexJson: internal_index, index, indexError, status, From 8065a82559f1bdbdf27574fc85705dcd6c1bda85 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 3 Oct 2024 12:09:13 +0800 Subject: [PATCH 08/53] ignore cache & logs --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f23bfa05951d..671748fd984f 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,7 @@ code/playwright/.cache/ code/bench-results/ /packs -code/.nx/cache \ No newline at end of file +code/.nx/cache +.nx/cache + +*storybook.log \ No newline at end of file From 7cdd11144136eddef75e2b70d639b22fa438ea16 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 9 Oct 2024 15:08:54 +0200 Subject: [PATCH 09/53] remove util dependency from instrumenter and test --- code/lib/instrumenter/package.json | 6 ++- code/lib/test/package.json | 6 ++- code/yarn.lock | 68 +----------------------------- 3 files changed, 9 insertions(+), 71 deletions(-) diff --git a/code/lib/instrumenter/package.json b/code/lib/instrumenter/package.json index 387aa9495021..3ac2d05b6d93 100644 --- a/code/lib/instrumenter/package.json +++ b/code/lib/instrumenter/package.json @@ -44,8 +44,7 @@ }, "dependencies": { "@storybook/global": "^5.0.0", - "@vitest/utils": "^2.0.5", - "util": "^0.12.4" + "@vitest/utils": "^2.0.5" }, "devDependencies": { "typescript": "^5.3.2" @@ -68,6 +67,9 @@ "@vitest/expect", "@vitest/spy", "@vitest/utils" + ], + "externals": [ + "util" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16" diff --git a/code/lib/test/package.json b/code/lib/test/package.json index 258b29a136ca..75970c8fa437 100644 --- a/code/lib/test/package.json +++ b/code/lib/test/package.json @@ -51,8 +51,7 @@ "@testing-library/jest-dom": "6.5.0", "@testing-library/user-event": "14.5.2", "@vitest/expect": "2.0.5", - "@vitest/spy": "2.0.5", - "util": "^0.12.4" + "@vitest/spy": "2.0.5" }, "devDependencies": { "chai": "^5.1.1", @@ -79,6 +78,9 @@ "@vitest/expect", "@vitest/spy", "@vitest/utils" + ], + "externals": [ + "util" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16" diff --git a/code/yarn.lock b/code/yarn.lock index 79822b6950b8..8c127189a5ae 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6067,7 +6067,6 @@ __metadata: browser-assert: "npm:^1.2.1" browser-dtector: "npm:^3.4.0" camelcase: "npm:^8.0.0" - chai: "npm:^4.4.1" cli-table3: "npm:^0.6.1" commander: "npm:^12.1.0" comment-parser: "npm:^1.4.1" @@ -6360,7 +6359,6 @@ __metadata: "@storybook/global": "npm:^5.0.0" "@vitest/utils": "npm:^2.0.5" typescript: "npm:^5.3.2" - util: "npm:^0.12.4" peerDependencies: storybook: "workspace:^" languageName: unknown @@ -7113,7 +7111,6 @@ __metadata: ts-dedent: "npm:^2.2.0" type-fest: "npm:~2.19" typescript: "npm:^5.3.2" - util: "npm:^0.12.4" peerDependencies: storybook: "workspace:^" languageName: unknown @@ -10130,13 +10127,6 @@ __metadata: languageName: node linkType: hard -"assertion-error@npm:^1.1.0": - version: 1.1.0 - resolution: "assertion-error@npm:1.1.0" - checksum: 10c0/25456b2aa333250f01143968e02e4884a34588a8538fbbf65c91a637f1dbfb8069249133cd2f4e530f10f624d206a664e7df30207830b659e9f5298b00a4099b - languageName: node - linkType: hard - "assertion-error@npm:^2.0.1": version: 2.0.1 resolution: "assertion-error@npm:2.0.1" @@ -11437,21 +11427,6 @@ __metadata: languageName: node linkType: hard -"chai@npm:^4.4.1": - version: 4.5.0 - resolution: "chai@npm:4.5.0" - dependencies: - assertion-error: "npm:^1.1.0" - check-error: "npm:^1.0.3" - deep-eql: "npm:^4.1.3" - get-func-name: "npm:^2.0.2" - loupe: "npm:^2.3.6" - pathval: "npm:^1.1.1" - type-detect: "npm:^4.1.0" - checksum: 10c0/b8cb596bd1aece1aec659e41a6e479290c7d9bee5b3ad63d2898ad230064e5b47889a3bc367b20100a0853b62e026e2dc514acf25a3c9385f936aa3614d4ab4d - languageName: node - linkType: hard - "chai@npm:^5.1.1": version: 5.1.1 resolution: "chai@npm:5.1.1" @@ -11568,15 +11543,6 @@ __metadata: languageName: node linkType: hard -"check-error@npm:^1.0.3": - version: 1.0.3 - resolution: "check-error@npm:1.0.3" - dependencies: - get-func-name: "npm:^2.0.2" - checksum: 10c0/94aa37a7315c0e8a83d0112b5bfb5a8624f7f0f81057c73e4707729cdd8077166c6aefb3d8e2b92c63ee130d4a2ff94bad46d547e12f3238cc1d78342a973841 - languageName: node - linkType: hard - "check-error@npm:^2.1.1": version: 2.1.1 resolution: "check-error@npm:2.1.1" @@ -12847,15 +12813,6 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^4.1.3": - version: 4.1.3 - resolution: "deep-eql@npm:4.1.3" - dependencies: - type-detect: "npm:^4.0.0" - checksum: 10c0/ff34e8605d8253e1bf9fe48056e02c6f347b81d9b5df1c6650a1b0f6f847b4a86453b16dc226b34f853ef14b626e85d04e081b022e20b00cd7d54f079ce9bbdd - languageName: node - linkType: hard - "deep-eql@npm:^5.0.1": version: 5.0.2 resolution: "deep-eql@npm:5.0.2" @@ -16003,7 +15960,7 @@ __metadata: languageName: node linkType: hard -"get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": +"get-func-name@npm:^2.0.1": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" checksum: 10c0/89830fd07623fa73429a711b9daecdb304386d237c71268007f788f113505ef1d4cc2d0b9680e072c5082490aec9df5d7758bf5ac6f1c37062855e8e3dc0b9df @@ -19270,15 +19227,6 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^2.3.6": - version: 2.3.7 - resolution: "loupe@npm:2.3.7" - dependencies: - get-func-name: "npm:^2.0.1" - checksum: 10c0/71a781c8fc21527b99ed1062043f1f2bb30bdaf54fa4cf92463427e1718bc6567af2988300bc243c1f276e4f0876f29e3cbf7b58106fdc186915687456ce5bf4 - languageName: node - linkType: hard - "loupe@npm:^3.1.0, loupe@npm:^3.1.1": version: 3.1.1 resolution: "loupe@npm:3.1.1" @@ -22534,13 +22482,6 @@ __metadata: languageName: node linkType: hard -"pathval@npm:^1.1.1": - version: 1.1.1 - resolution: "pathval@npm:1.1.1" - checksum: 10c0/f63e1bc1b33593cdf094ed6ff5c49c1c0dc5dc20a646ca9725cc7fe7cd9995002d51d5685b9b2ec6814342935748b711bafa840f84c0bb04e38ff40a335c94dc - languageName: node - linkType: hard - "pathval@npm:^2.0.0": version: 2.0.0 resolution: "pathval@npm:2.0.0" @@ -27457,13 +27398,6 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:^4.0.0, type-detect@npm:^4.1.0": - version: 4.1.0 - resolution: "type-detect@npm:4.1.0" - checksum: 10c0/df8157ca3f5d311edc22885abc134e18ff8ffbc93d6a9848af5b682730ca6a5a44499259750197250479c5331a8a75b5537529df5ec410622041650a7f293e2a - languageName: node - linkType: hard - "type-fest@npm:~2.19": version: 2.19.0 resolution: "type-fest@npm:2.19.0" From a6b2f30a62d3166f3b2ca852a7becc5b9dcd82a1 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 9 Oct 2024 15:09:02 +0200 Subject: [PATCH 10/53] remove unused chai from core --- code/core/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/code/core/package.json b/code/core/package.json index a5f179a43316..f549ff2c5272 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -340,7 +340,6 @@ "boxen": "^7.1.1", "browser-dtector": "^3.4.0", "camelcase": "^8.0.0", - "chai": "^4.4.1", "cli-table3": "^0.6.1", "commander": "^12.1.0", "comment-parser": "^1.4.1", From b96bcb701e254378a1ec60e1838c1ba23842e02d Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Fri, 11 Oct 2024 19:01:52 +0800 Subject: [PATCH 11/53] Simplified design WIP --- .../manager/components/sidebar/TagsFilter.tsx | 67 ++++++++++--------- .../sidebar/TagsFilterPanel.stories.tsx | 13 +--- .../components/sidebar/TagsFilterPanel.tsx | 49 +++----------- 3 files changed, 46 insertions(+), 83 deletions(-) diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index 09118ad544d1..f9e6b5dbb423 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -1,9 +1,12 @@ -import React, { useState, useEffect } from 'react'; -import { Badge, IconButton, WithTooltip } from '@storybook/components'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { Badge, IconButton, WithTooltip } from '@storybook/core/components'; +import { styled } from '@storybook/core/theming'; import { FilterIcon } from '@storybook/icons'; -import type { API } from '@storybook/manager-api'; -import { styled } from '@storybook/theming'; -import type { Tag, StoryIndex } from '@storybook/types'; +import type { StoryIndex, Tag } from '@storybook/types'; + +import type { API } from '@storybook/core/manager-api'; + import { TagsFilterPanel } from './TagsFilterPanel'; const TAGS_FILTER = 'tags-filter'; @@ -43,28 +46,46 @@ export const TagsFilter = ({ initialSelectedTags = [], }: TagsFilterProps) => { const [selectedTags, setSelectedTags] = useState(initialSelectedTags); - const [exclude, setExclude] = useState(false); const [expanded, setExpanded] = useState(false); const tagsActive = selectedTags.length > 0; useEffect(() => { api.experimental_setFilter(TAGS_FILTER, (item) => { - if (selectedTags.length === 0) return true; + if (selectedTags.length === 0) { + return true; + } - const hasSelectedTags = selectedTags.some((tag) => item.tags?.includes(tag)); - return exclude ? !hasSelectedTags : hasSelectedTags; + return selectedTags.some((tag) => item.tags?.includes(tag)); }); - const tagsParam = selectedTags.join(','); - const [includeTags, excludeTags] = exclude ? [null, tagsParam] : [tagsParam, null]; - updateQueryParams({ includeTags, excludeTags }); - }, [api, selectedTags, exclude, updateQueryParams]); + const includeTags = selectedTags.join(','); + updateQueryParams({ includeTags }); + }, [api, selectedTags, updateQueryParams]); const allTags = Object.values(indexJson.entries).reduce((acc, entry) => { entry.tags?.forEach((tag: Tag) => acc.add(tag)); return acc; }, new Set()); + const toggleTag = useCallback( + (tag: string) => { + if (selectedTags.includes(tag)) { + setSelectedTags(selectedTags.filter((t) => t !== tag)); + } else { + setSelectedTags([...selectedTags, tag]); + } + }, + [selectedTags, setSelectedTags] + ); + + const handleToggleExpand = useCallback( + (event: React.SyntheticEvent): void => { + event.preventDefault(); + setExpanded(!expanded); + }, + [expanded, setExpanded] + ); + return ( { - if (selectedTags.includes(tag)) { - setSelectedTags(selectedTags.filter((t) => t !== tag)); - } else { - setSelectedTags([...selectedTags, tag]); - } - }} - toggleExclude={() => setExclude(!exclude)} + toggleTag={toggleTag} /> )} > - { - event.preventDefault(); - setExpanded(!expanded); - }} - > + {selectedTags.length > 0 && {selectedTags.length}} diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx index ef0b9e47002d..894e7eafc211 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx @@ -6,9 +6,7 @@ import { TagsFilterPanel } from './TagsFilterPanel'; const meta = { component: TagsFilterPanel, args: { - exclude: false, toggleTag: fn(), - toggleExclude: fn(), }, tags: ['hoho'], } satisfies Meta; @@ -31,16 +29,9 @@ export const Default: Story = { }, }; -export const Exclude: Story = { - args: { - ...Default.args, - exclude: true, - }, -}; - export const BuiltInTags: Story = { args: { - allTags: [...Default.args.allTags, 'dev', 'autodocs'], + allTags: [...Default.args.allTags, 'dev', 'autodocs', 'play-fn'], selectedTags: ['tag1', 'tag3'], }, }; @@ -48,6 +39,6 @@ export const BuiltInTags: Story = { export const BuiltInTagsSelected: Story = { args: { ...BuiltInTags.args, - selectedTags: ['tag1', 'tag3', 'autodocs'], + selectedTags: ['tag1', 'tag3', 'autodocs', 'play-fn'], }, }; diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx index 0f82b8baef71..93b884dff55d 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx @@ -1,10 +1,11 @@ import type { ChangeEvent } from 'react'; -import React, { useState } from 'react'; -import { transparentize } from 'polished'; -import { styled } from '@storybook/theming'; -import { CollapseIcon } from './components/CollapseIcon'; +import React from 'react'; + +import { styled } from '@storybook/core/theming'; import type { Tag } from '@storybook/types'; +import { transparentize } from 'polished'; + const BUILT_IN_TAGS = new Set([ 'dev', 'autodocs', @@ -55,9 +56,7 @@ const Label = styled.label({ interface TagsFilterPanelProps { allTags: Tag[]; selectedTags: Tag[]; - exclude: boolean; toggleTag: (tag: Tag) => void; - toggleExclude: () => void; } interface TagsListProps { @@ -95,50 +94,18 @@ const Wrapper = styled.div({ }, }); -export const TagsFilterPanel = ({ - allTags, - selectedTags, - exclude, - toggleTag, - toggleExclude, -}: TagsFilterPanelProps) => { - const userTags = allTags.filter((tag) => !BUILT_IN_TAGS.has(tag)).toSorted(); - const builtInTags = allTags.filter((tag) => BUILT_IN_TAGS.has(tag)).toSorted(); - const [builtinsExpanded, setBuiltinsExpanded] = useState( - selectedTags.some((tag) => BUILT_IN_TAGS.has(tag)) - ); +export const TagsFilterPanel = ({ allTags, selectedTags, toggleTag }: TagsFilterPanelProps) => { + const userTags = allTags.filter((tag) => tag === 'play-fn' || !BUILT_IN_TAGS.has(tag)).toSorted(); return (
{userTags.length === 0 ? ( - 'No tags defined' + <>There are no tags. Use tags to organize and filter your Storybook. ) : ( - Tags {exclude ? 'does not contain' : 'contains'} )} - {builtInTags.length > 0 && ( - <> - { - event.preventDefault(); - setBuiltinsExpanded(!builtinsExpanded); - }} - aria-expanded={builtinsExpanded} - > - - Built-in tags - - {builtinsExpanded ? ( - - - - ) : null} - - )}
); }; From e319c524184f8612a540becff06291f131e72e30 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 11 Oct 2024 15:39:53 +0200 Subject: [PATCH 12/53] Fix search bar structure and styling --- .../src/manager/components/sidebar/Search.tsx | 128 +++++++----------- .../components/sidebar/Sidebar.stories.tsx | 1 + .../manager/components/sidebar/Sidebar.tsx | 69 ++++++++-- 3 files changed, 106 insertions(+), 92 deletions(-) diff --git a/code/core/src/manager/components/sidebar/Search.tsx b/code/core/src/manager/components/sidebar/Search.tsx index d09863a28aab..3b9884dd1497 100644 --- a/code/core/src/manager/components/sidebar/Search.tsx +++ b/code/core/src/manager/components/sidebar/Search.tsx @@ -1,9 +1,9 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { type ReactNode, useCallback, useRef, useState } from 'react'; -import { IconButton, TooltipNote, WithTooltip } from '@storybook/core/components'; +import { IconButton } from '@storybook/core/components'; import { styled } from '@storybook/core/theming'; import { global } from '@storybook/global'; -import { CloseIcon, PlusIcon, SearchIcon } from '@storybook/icons'; +import { CloseIcon, SearchIcon } from '@storybook/icons'; import { shortcutToHumanString, useStorybookApi } from '@storybook/core/manager-api'; @@ -15,7 +15,6 @@ import Fuse from 'fuse.js'; import { getGroupStatus, getHighestStatus } from '../../utils/status'; import { scrollIntoView, searchItem } from '../../utils/tree'; import { useLayout } from '../layout/LayoutProvider'; -import { CreateNewStoryFileModal } from './CreateNewStoryFileModal'; import { DEFAULT_REF_ID } from './Sidebar'; import type { CombinedDataset, @@ -54,10 +53,6 @@ const SearchBar = styled.div({ columnGap: 6, }); -const TooltipNoteWrapper = styled(TooltipNote)({ - margin: 0, -}); - const ScreenReaderLabel = styled.label({ position: 'absolute', left: -10000, @@ -67,49 +62,47 @@ const ScreenReaderLabel = styled.label({ overflow: 'hidden', }); -const CreateNewStoryButton = styled(IconButton)(({ theme }) => ({ - color: theme.color.mediumdark, +const SearchField = styled.div(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: 2, + flexGrow: 1, + height: 32, + width: '100%', + boxShadow: `${theme.button.border} 0 0 0 1px inset`, + borderRadius: theme.appBorderRadius + 2, + + '&:has(input:focus), &:has(input:active)': { + boxShadow: `${theme.color.secondary} 0 0 0 1px inset`, + background: theme.background.app, + }, })); -const SearchIconWrapper = styled.div(({ theme }) => ({ - position: 'absolute', - top: 0, - left: 8, - zIndex: 1, - pointerEvents: 'none', +const IconWrapper = styled.div(({ theme, onClick }) => ({ + cursor: onClick ? 'pointer' : 'default', + flex: '0 0 28px', + height: '100%', + pointerEvents: onClick ? 'auto' : 'none', color: theme.textMutedColor, display: 'flex', alignItems: 'center', - height: '100%', + justifyContent: 'center', })); -const SearchField = styled.div({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - position: 'relative', -}); - const Input = styled.input(({ theme }) => ({ appearance: 'none', height: 28, - paddingLeft: 28, - paddingRight: 28, + width: '100%', + padding: 0, border: 0, - boxShadow: `${theme.button.border} 0 0 0 1px inset`, background: 'transparent', - borderRadius: 4, fontSize: `${theme.typography.size.s1 + 1}px`, fontFamily: 'inherit', transition: 'all 150ms', color: theme.color.defaultText, - width: '100%', + outline: 0, - '&:focus, &:active': { - outline: 0, - borderColor: theme.color.secondary, - background: theme.background.app, - }, '&::placeholder': { color: theme.textMutedColor, opacity: 1, @@ -133,11 +126,9 @@ const Input = styled.input(({ theme }) => ({ })); const FocusKey = styled.code(({ theme }) => ({ - position: 'absolute', - top: 6, - right: 9, + margin: 5, + marginTop: 6, height: 16, - zIndex: 1, lineHeight: '16px', textAlign: 'center', fontSize: '11px', @@ -153,44 +144,35 @@ const FocusKeyCmd = styled.span({ fontSize: '14px', }); -const ClearIcon = styled.div(({ theme }) => ({ - position: 'absolute', - top: 0, - right: 8, - zIndex: 1, - color: theme.textMutedColor, - cursor: 'pointer', +const Actions = styled.div({ display: 'flex', alignItems: 'center', - height: '100%', -})); +}); const FocusContainer = styled.div({ outline: 0 }); -const isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT'; -const isRendererReact = global.STORYBOOK_RENDERER === 'react'; - export const Search = React.memo<{ children: SearchChildrenFn; dataset: CombinedDataset; enableShortcuts?: boolean; getLastViewed: () => Selection[]; initialQuery?: string; - showCreateStoryButton?: boolean; + searchBarContent?: ReactNode; + searchFieldContent?: ReactNode; }>(function Search({ children, dataset, enableShortcuts = true, getLastViewed, initialQuery = '', - showCreateStoryButton = isDevelopment && isRendererReact, + searchBarContent, + searchFieldContent, }) { const api = useStorybookApi(); const inputRef = useRef(null); const [inputPlaceholder, setPlaceholder] = useState('Find components'); const [allComponents, showAllComponents] = useState(false); const searchShortcut = api ? shortcutToHumanString(api.getShortcutKeys().search) : '/'; - const [isFileSearchModalOpen, setIsFileSearchModalOpen] = useState(false); const makeFuse = useCallback(() => { const list = dataset.entries.reduce((acc, [refId, { index, status }]) => { @@ -406,9 +388,9 @@ export const Search = React.memo<{ {...getRootProps({ refKey: '' }, { suppressRefError: true })} className="search-field" > - + - + {!isMobile && enableShortcuts && !isOpen && ( @@ -421,34 +403,16 @@ export const Search = React.memo<{ )} )} - {isOpen && ( - clearSelection()}> - - - )} + + {isOpen && ( + clearSelection()}> + + + )} + {searchFieldContent} + - {showCreateStoryButton && ( - <> - } - > - { - setIsFileSearchModalOpen(true); - }} - variant="outline" - > - - - - - - )} + {searchBarContent} {children({ diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index 972606b79598..77086b886cce 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -56,6 +56,7 @@ const meta = { menu, extra: [] as Addon_SidebarTopType[], index: index, + indexJson: { entries: {}, v: 6 }, storyId, refId: DEFAULT_REF_ID, refs: {}, diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index 3233b32acbb5..8409e3ace435 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -1,19 +1,28 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; -import { ScrollArea, Spaced } from '@storybook/core/components'; +import { + IconButton, + ScrollArea, + Spaced, + TooltipNote, + WithTooltip, +} from '@storybook/core/components'; import { styled } from '@storybook/core/theming'; import type { API_LoadedRefData, Addon_SidebarTopType, StoryIndex } from '@storybook/core/types'; +import { global } from '@storybook/global'; +import { PlusIcon } from '@storybook/icons'; -import { useStorybookApi, type State } from '@storybook/core/manager-api'; +import { type State, useStorybookApi } from '@storybook/core/manager-api'; import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; +import { CreateNewStoryFileModal } from './CreateNewStoryFileModal'; import { Explorer } from './Explorer'; import type { HeadingProps } from './Heading'; import { Heading } from './Heading'; -import { TagsFilter } from './TagsFilter'; import { Search } from './Search'; import { SearchResults } from './SearchResults'; import { SidebarBottom } from './SidebarBottom'; +import { TagsFilter } from './TagsFilter'; import type { CombinedDataset, Selection } from './types'; import { useLastViewed } from './useLastViewed'; @@ -58,6 +67,17 @@ const Bottom = styled.div(({ theme }) => ({ }, })); +const TooltipNoteWrapper = styled(TooltipNote)({ + margin: 0, +}); + +const CreateNewStoryButton = styled(IconButton)(({ theme }) => ({ + color: theme.color.mediumdark, + width: 32, + height: 32, + borderRadius: theme.appBorderRadius + 2, +})); + const Swap = React.memo(function Swap({ children, condition, @@ -111,6 +131,10 @@ const updateQueryParams = (params: Record) => { }); window.history.pushState({}, '', url); }; + +const isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT'; +const isRendererReact = global.STORYBOOK_RENDERER === 'react'; + export interface SidebarProps extends API_LoadedRefData { refs: State['refs']; status: State['status']; @@ -124,7 +148,6 @@ export interface SidebarProps extends API_LoadedRefData { showCreateStoryButton?: boolean; indexJson?: StoryIndex; } - export const Sidebar = React.memo(function Sidebar({ // @ts-expect-error (non strict) storyId = null, @@ -140,8 +163,9 @@ export const Sidebar = React.memo(function Sidebar({ enableShortcuts = true, refs = {}, onMenuClick, - showCreateStoryButton, + showCreateStoryButton = isDevelopment && isRendererReact, }: SidebarProps) { + const [isFileSearchModalOpen, setIsFileSearchModalOpen] = useState(false); // @ts-expect-error (non strict) const selected: Selection = useMemo(() => storyId && { storyId, refId }, [storyId, refId]); const dataset = useCombination(index, indexError, previewInitialized, status, refs); @@ -162,13 +186,38 @@ export const Sidebar = React.memo(function Sidebar({ isLoading={isLoading} onMenuClick={onMenuClick} /> - {index && ( - - )} + } + > + { + setIsFileSearchModalOpen(true); + }} + variant="outline" + > + + + + + + ) + } + searchFieldContent={ + indexJson && ( + + ) + } {...lastViewedProps} > {({ From 55295300e44c5e4768c46fdb37fcce66b522a0f8 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sat, 12 Oct 2024 00:32:17 +0800 Subject: [PATCH 13/53] Tweaks --- code/core/src/manager/components/sidebar/Search.tsx | 2 ++ code/core/src/manager/components/sidebar/TagsFilter.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/code/core/src/manager/components/sidebar/Search.tsx b/code/core/src/manager/components/sidebar/Search.tsx index 3b9884dd1497..16a7ba0f151e 100644 --- a/code/core/src/manager/components/sidebar/Search.tsx +++ b/code/core/src/manager/components/sidebar/Search.tsx @@ -138,6 +138,7 @@ const FocusKey = styled.code(({ theme }) => ({ display: 'flex', alignItems: 'center', gap: 4, + flexShrink: 0, })); const FocusKeyCmd = styled.span({ @@ -147,6 +148,7 @@ const FocusKeyCmd = styled.span({ const Actions = styled.div({ display: 'flex', alignItems: 'center', + gap: 2, }); const FocusContainer = styled.div({ outline: 0 }); diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index f9e6b5dbb423..38d415b4cc41 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -35,7 +35,7 @@ const Count = styled(Badge)(({ theme }) => ({ export interface TagsFilterProps { api: API; indexJson: StoryIndex; - updateQueryParams: (params: Record) => void; + updateQueryParams: (params: Record) => void; initialSelectedTags?: Tag[]; } From e8a505cd4edbba8f0cbec6e6f138b6d49938bc66 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sat, 12 Oct 2024 09:40:29 +0800 Subject: [PATCH 14/53] Tags filter: Use built in menu components --- .gitignore | 3 + .../components/sidebar/TagsFilter.stories.tsx | 21 ++- .../manager/components/sidebar/TagsFilter.tsx | 2 + .../sidebar/TagsFilterPanel.stories.tsx | 3 + .../components/sidebar/TagsFilterPanel.tsx | 131 +++++++----------- 5 files changed, 72 insertions(+), 88 deletions(-) diff --git a/.gitignore b/.gitignore index aabce1916add..0724b9827912 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,9 @@ code/bench-results/ /packs code/.nx/cache +code/.vite-inspect .nx/cache +!**/fixtures/**/yarn.lock +code/core/report *storybook.log diff --git a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx index 74b53b30c838..eb277f33b3da 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx @@ -6,6 +6,13 @@ import { TagsFilter } from './TagsFilter'; const meta = { component: TagsFilter, tags: ['haha'], + args: { + api: { + experimental_setFilter: fn(), + getDocsUrl: () => 'https://storybook.js.org/docs/', + } as any, + updateQueryParams: fn(), + }, } satisfies Meta; export default meta; @@ -14,16 +21,12 @@ type Story = StoryObj; export const Closed: Story = { args: { - api: { - experimental_setFilter: fn(), - } as any, indexJson: { v: 6, entries: { 'c1-s1': { tags: ['A', 'B', 'C', 'dev'] } as any, }, }, - updateQueryParams: fn(), }, }; @@ -46,3 +49,13 @@ export const OpenWithSelection: Story = { ...ClosedWithSelection, play: Open.play, }; + +export const OpenEmpty: Story = { + args: { + indexJson: { + v: 6, + entries: {}, + }, + }, + play: Open.play, +}; diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index 38d415b4cc41..e7e7a4595d71 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -93,11 +93,13 @@ export const TagsFilter = ({ onVisibleChange={setExpanded} tooltip={() => ( )} + closeOnOutsideClick > diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx index 894e7eafc211..108b39440973 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx @@ -7,6 +7,9 @@ const meta = { component: TagsFilterPanel, args: { toggleTag: fn(), + api: { + getDocsUrl: () => 'https://storybook.js.org/docs/', + } as any, }, tags: ['hoho'], } satisfies Meta; diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx index 93b884dff55d..02984753dce7 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx @@ -1,10 +1,11 @@ -import type { ChangeEvent } from 'react'; import React from 'react'; -import { styled } from '@storybook/core/theming'; +import { TooltipLinkList } from '@storybook/core/components'; +import { styled, useTheme } from '@storybook/core/theming'; +import { ShareAltIcon } from '@storybook/icons'; import type { Tag } from '@storybook/types'; -import { transparentize } from 'polished'; +import type { API } from '@storybook/core/manager-api'; const BUILT_IN_TAGS = new Set([ 'dev', @@ -15,97 +16,59 @@ const BUILT_IN_TAGS = new Set([ 'play-fn', ]); -const CollapseButton = styled.button(({ theme }) => ({ - all: 'unset', - display: 'flex', - padding: '0px 8px', - borderRadius: 4, - transition: 'color 150ms, box-shadow 150ms', - gap: 6, - alignItems: 'center', - cursor: 'pointer', - height: 28, - - '&:hover, &:focus': { - outline: 'none', - background: transparentize(0.93, theme.color.secondary), - }, -})); - -const Text = styled.span({ - '[aria-readonly=true] &': { - opacity: 0.5, - }, -}); - -const Label = styled.label({ - lineHeight: '20px', - alignItems: 'center', - marginBottom: 8, - - '&:last-child': { - marginBottom: 0, - }, - - input: { - margin: 0, - marginRight: 6, - }, +const Wrapper = styled.div({ + minWidth: 180, + maxWidth: 220, }); interface TagsFilterPanelProps { + api: API; allTags: Tag[]; selectedTags: Tag[]; toggleTag: (tag: Tag) => void; } -interface TagsListProps { - tags: Tag[]; - selectedTags: Tag[]; - toggleTag: (tag: Tag) => void; -} - -const TagsList = ({ tags, selectedTags, toggleTag }: TagsListProps) => { - return tags.map((tag) => { - const checked = selectedTags.includes(tag); - const id = `tag-${tag}`; - return ( - - ); - }); -}; - -const Wrapper = styled.div({ - padding: 10, - label: { - display: 'flex', - }, -}); - -export const TagsFilterPanel = ({ allTags, selectedTags, toggleTag }: TagsFilterPanelProps) => { +export const TagsFilterPanel = ({ + api, + allTags, + selectedTags, + toggleTag, +}: TagsFilterPanelProps) => { + const theme = useTheme(); const userTags = allTags.filter((tag) => tag === 'play-fn' || !BUILT_IN_TAGS.has(tag)).toSorted(); + const docsUrl = api.getDocsUrl({ subpath: 'writing-stories/tags' }); + const items = + userTags.length === 0 + ? [ + { + id: 'no-tags', + title: 'There are no tags. Use tags to organize and filter your Storybook.', + isIndented: false, + style: { + borderBottom: `4px solid ${theme.appBorderColor}`, + }, + }, + { + id: 'tags-docs', + title: 'Learn how to add tags', + icon: , + href: docsUrl, + }, + ] + : userTags.map((tag) => { + const checked = selectedTags.includes(tag); + const id = `tag-${tag}`; + return { + id, + title: tag, + right: , + onClick: () => toggleTag(tag), + }; + }); return ( -
- {userTags.length === 0 ? ( - <>There are no tags. Use tags to organize and filter your Storybook. - ) : ( - - - - )} -
+ + + ); }; From 2b53b6ead9f5052e6183193393b143043314dbb0 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sat, 12 Oct 2024 09:56:47 +0800 Subject: [PATCH 15/53] Cleanup --- .../src/core-server/presets/common-manager.ts | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/code/core/src/core-server/presets/common-manager.ts b/code/core/src/core-server/presets/common-manager.ts index 6b81e2988762..0bdf7cde0031 100644 --- a/code/core/src/core-server/presets/common-manager.ts +++ b/code/core/src/core-server/presets/common-manager.ts @@ -1,23 +1,10 @@ import { global } from '@storybook/global'; -import type { Tag } from '@storybook/types'; -const TAG_FILTERS = 'tag-filters'; import { addons } from '@storybook/core/manager-api'; +const TAG_FILTERS = 'tag-filters'; const STATIC_FILTER = 'static-filter'; -const parseTags = (tags?: string) => { - if (!tags) return undefined; - return tags.split(',').reduce( - (acc, tag) => { - tag.trim(); - acc[tag] = true; - return acc; - }, - {} as Record - ); -}; - addons.register(TAG_FILTERS, (api) => { // FIXME: this ensures the filter is applied after the first render // to avoid a strange race condition in Webkit only. @@ -33,24 +20,6 @@ addons.register(TAG_FILTERS, (api) => { ); api.experimental_setFilter(STATIC_FILTER, (item) => { - // const tags = item.tags || []; - // return tags.filter((tag) => staticExludeTags[tag]).length === 0; - // }); - - // api.experimental_setFilter(UI_FILTER, (item) => { - // const tags = item.tags || []; - // const { queryParams } = api.getUrlState(); - // const tagToInclude = parseTags(queryParams.tagsFilter); - - // if (!tagToInclude) return true; - - // let include = true; - // include = tags.some((tag) => tagToInclude[tag)); - - // if (excludeTags) { - // include = !tags.some((tag) => excludeTags.includes(tag)); - // } - // return include; const tags = item.tags ?? []; return ( // we can filter out the primary story, but we still want to show autodocs From 25d98e52fbf9b4b2dd91bcea09ec7d33774cf3d9 Mon Sep 17 00:00:00 2001 From: vctqs1 Date: Sat, 12 Oct 2024 18:48:19 +0700 Subject: [PATCH 16/53] resolve export from import --- code/core/src/csf-tools/ConfigFile.test.ts | 28 ++++++++++++++++++++++ code/core/src/csf-tools/ConfigFile.ts | 27 ++++++++++++++++++++- code/core/src/csf-tools/parameters.data.ts | 1 + code/package.json | 2 +- code/vitest.config.ts | 1 + 5 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 code/core/src/csf-tools/parameters.data.ts diff --git a/code/core/src/csf-tools/ConfigFile.test.ts b/code/core/src/csf-tools/ConfigFile.test.ts index fe841c49f488..a540ed0d7ceb 100644 --- a/code/core/src/csf-tools/ConfigFile.test.ts +++ b/code/core/src/csf-tools/ConfigFile.test.ts @@ -1308,4 +1308,32 @@ describe('ConfigFile', () => { ); }); }); + + describe.only('parse', () => { + it("export { X } with X is import { X } from 'another-file'", () => { + const source = dedent` + import type { StorybookConfig } from '@storybook/react-webpack5'; + import { parameters } from './parameters.data'; + + export { parameters }; + + const config: StorybookConfig = { + addons: [ + 'foo', + { name: 'bar', options: {} }, + ], + "otherField": [ + "foo", + { "name": 'bar', options: {} }, + ], + } + export default config; + `; + const config = loadConfig(source).parse() + + // ensure config._exportDecls vs config._exports has 'parameters' + expect(config._exportDecls['parameters']).toBeTruthy() + expect(config._exports['parameters']).toBeTruthy() + }) + }) }); diff --git a/code/core/src/csf-tools/ConfigFile.ts b/code/core/src/csf-tools/ConfigFile.ts index 5cb0d28234eb..ba21b0bd3331 100644 --- a/code/core/src/csf-tools/ConfigFile.ts +++ b/code/core/src/csf-tools/ConfigFile.ts @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { readFile, writeFile } from 'node:fs/promises'; +import { readFileSync } from 'node:fs'; import { type RecastOptions, @@ -13,6 +14,7 @@ import { import { dedent } from 'ts-dedent'; import type { PrintResultType } from './PrintResultType'; +import path from 'node:path'; const logger = console; @@ -102,7 +104,24 @@ const _findVarDeclarator = ( ): t.VariableDeclarator | null | undefined => { let declarator: t.VariableDeclarator | null | undefined = null; let declarations: t.VariableDeclarator[] | null = null; + program.body.find((node: t.Node) => { + if(t.isImportDeclaration(node)) { + + node.specifiers.forEach((specifier) => { + + if(t.isImportSpecifier(specifier) && (specifier as t.ImportSpecifier).local?.name === identifier) { + const importSource = node.source.value; // the source module of the import + const importedConfig = readConfigSync(require.resolve(path.resolve(__dirname, `${importSource}.ts`))); + + const importedFileAST = importedConfig._ast.program; // Adjust this depending on how your importedConfig is structured + declarator = _findVarDeclarator(((specifier as t.ImportSpecifier).imported as t.Identifier).name, importedFileAST) + + return true; // stop looking + } + }) + } + if (t.isVariableDeclaration(node)) { declarations = node.declarations; } else if (t.isExportNamedDeclaration(node) && t.isVariableDeclaration(node.declaration)) { @@ -248,8 +267,9 @@ export class ConfigFile { ) { const { name: localName } = spec.local; const { name: exportName } = spec.exported; + const decl = _findVarDeclarator(localName, parent as t.Program) as any; - self._exports[exportName] = decl.init; + self._exports[exportName] = decl?.init; self._exportDecls[exportName] = decl; } }); @@ -899,6 +919,11 @@ export const readConfig = async (fileName: string) => { return loadConfig(code, fileName).parse(); }; +export const readConfigSync = (fileName: string) => { + const code = readFileSync(fileName, 'utf-8').toString(); + return loadConfig(code).parse(); +}; + export const writeConfig = async (config: ConfigFile, fileName?: string) => { const fname = fileName || config.fileName; diff --git a/code/core/src/csf-tools/parameters.data.ts b/code/core/src/csf-tools/parameters.data.ts new file mode 100644 index 000000000000..93e5d6e28c78 --- /dev/null +++ b/code/core/src/csf-tools/parameters.data.ts @@ -0,0 +1 @@ +export const parameters = {} \ No newline at end of file diff --git a/code/package.json b/code/package.json index 5dab4f6f2052..1f3018069ed2 100644 --- a/code/package.json +++ b/code/package.json @@ -52,7 +52,7 @@ "storybook:vitest": "yarn test:watch --project storybook-ui", "storybook:vitest:inspect": "INSPECT=true yarn test --project storybook-ui", "task": "yarn --cwd ../scripts task", - "test": "NODE_OPTIONS=--max_old_space_size=4096 vitest run", + "test": "NODE_OPTIONS=--max_old_space_size=4096 vitest run src/csf-tools/ConfigFile.test.ts", "test:watch": "NODE_OPTIONS=--max_old_space_size=4096 vitest watch" }, "husky": { diff --git a/code/vitest.config.ts b/code/vitest.config.ts index cabfb4e8b0ce..b848d9cc58bb 100644 --- a/code/vitest.config.ts +++ b/code/vitest.config.ts @@ -2,6 +2,7 @@ import { coverageConfigDefaults, defineConfig } from 'vitest/config'; export default defineConfig({ test: { + include: ["code/core/src/csf-tools/ConfigFile.test.ts"], coverage: { all: false, provider: 'istanbul', From 6a517f4135694289dabba37bbf38d6978eded9f5 Mon Sep 17 00:00:00 2001 From: vctqs1 Date: Sat, 12 Oct 2024 18:52:17 +0700 Subject: [PATCH 17/53] add step to check decl --- code/core/src/csf-tools/ConfigFile.test.ts | 8 ++++---- code/core/src/csf-tools/ConfigFile.ts | 24 ++++++---------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/code/core/src/csf-tools/ConfigFile.test.ts b/code/core/src/csf-tools/ConfigFile.test.ts index a540ed0d7ceb..d5fcfe8db80a 100644 --- a/code/core/src/csf-tools/ConfigFile.test.ts +++ b/code/core/src/csf-tools/ConfigFile.test.ts @@ -1313,9 +1313,9 @@ describe('ConfigFile', () => { it("export { X } with X is import { X } from 'another-file'", () => { const source = dedent` import type { StorybookConfig } from '@storybook/react-webpack5'; - import { parameters } from './parameters.data'; + import { path } from 'path'; - export { parameters }; + export { path }; const config: StorybookConfig = { addons: [ @@ -1332,8 +1332,8 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse() // ensure config._exportDecls vs config._exports has 'parameters' - expect(config._exportDecls['parameters']).toBeTruthy() - expect(config._exports['parameters']).toBeTruthy() + expect(config._exportDecls['path']).toBe(undefined) + expect(config._exports['path']).toBe(undefined) }) }) }); diff --git a/code/core/src/csf-tools/ConfigFile.ts b/code/core/src/csf-tools/ConfigFile.ts index ba21b0bd3331..93aabe663330 100644 --- a/code/core/src/csf-tools/ConfigFile.ts +++ b/code/core/src/csf-tools/ConfigFile.ts @@ -106,22 +106,6 @@ const _findVarDeclarator = ( let declarations: t.VariableDeclarator[] | null = null; program.body.find((node: t.Node) => { - if(t.isImportDeclaration(node)) { - - node.specifiers.forEach((specifier) => { - - if(t.isImportSpecifier(specifier) && (specifier as t.ImportSpecifier).local?.name === identifier) { - const importSource = node.source.value; // the source module of the import - const importedConfig = readConfigSync(require.resolve(path.resolve(__dirname, `${importSource}.ts`))); - - const importedFileAST = importedConfig._ast.program; // Adjust this depending on how your importedConfig is structured - declarator = _findVarDeclarator(((specifier as t.ImportSpecifier).imported as t.Identifier).name, importedFileAST) - - return true; // stop looking - } - }) - } - if (t.isVariableDeclaration(node)) { declarations = node.declarations; } else if (t.isExportNamedDeclaration(node) && t.isVariableDeclaration(node.declaration)) { @@ -269,8 +253,12 @@ export class ConfigFile { const { name: exportName } = spec.exported; const decl = _findVarDeclarator(localName, parent as t.Program) as any; - self._exports[exportName] = decl?.init; - self._exportDecls[exportName] = decl; + // decl can be empty in case X from `import { X } from ....` because it is not handled in _findVarDeclarator + if(decl) { + self._exports[exportName] = decl?.init; + self._exportDecls[exportName] = decl; + } + } }); } else { From 9faa98cacb907e26a136b093b37dde9923b3f98b Mon Sep 17 00:00:00 2001 From: vctqs1 Date: Sat, 12 Oct 2024 18:54:05 +0700 Subject: [PATCH 18/53] add step to check decl --- code/core/src/csf-tools/ConfigFile.test.ts | 2 +- code/core/src/csf-tools/ConfigFile.ts | 2 +- code/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/core/src/csf-tools/ConfigFile.test.ts b/code/core/src/csf-tools/ConfigFile.test.ts index d5fcfe8db80a..089cbaa8e6ea 100644 --- a/code/core/src/csf-tools/ConfigFile.test.ts +++ b/code/core/src/csf-tools/ConfigFile.test.ts @@ -1309,7 +1309,7 @@ describe('ConfigFile', () => { }); }); - describe.only('parse', () => { + describe('parse', () => { it("export { X } with X is import { X } from 'another-file'", () => { const source = dedent` import type { StorybookConfig } from '@storybook/react-webpack5'; diff --git a/code/core/src/csf-tools/ConfigFile.ts b/code/core/src/csf-tools/ConfigFile.ts index 93aabe663330..8d52c14bc7da 100644 --- a/code/core/src/csf-tools/ConfigFile.ts +++ b/code/core/src/csf-tools/ConfigFile.ts @@ -255,7 +255,7 @@ export class ConfigFile { const decl = _findVarDeclarator(localName, parent as t.Program) as any; // decl can be empty in case X from `import { X } from ....` because it is not handled in _findVarDeclarator if(decl) { - self._exports[exportName] = decl?.init; + self._exports[exportName] = decl.init; self._exportDecls[exportName] = decl; } diff --git a/code/package.json b/code/package.json index 1f3018069ed2..5dab4f6f2052 100644 --- a/code/package.json +++ b/code/package.json @@ -52,7 +52,7 @@ "storybook:vitest": "yarn test:watch --project storybook-ui", "storybook:vitest:inspect": "INSPECT=true yarn test --project storybook-ui", "task": "yarn --cwd ../scripts task", - "test": "NODE_OPTIONS=--max_old_space_size=4096 vitest run src/csf-tools/ConfigFile.test.ts", + "test": "NODE_OPTIONS=--max_old_space_size=4096 vitest run", "test:watch": "NODE_OPTIONS=--max_old_space_size=4096 vitest watch" }, "husky": { From 1e69f85d11d35191c2bed9508a111e62a058610f Mon Sep 17 00:00:00 2001 From: vctqs1 Date: Sat, 12 Oct 2024 19:00:27 +0700 Subject: [PATCH 19/53] add step to check decl --- code/core/src/csf-tools/ConfigFile.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/code/core/src/csf-tools/ConfigFile.ts b/code/core/src/csf-tools/ConfigFile.ts index 8d52c14bc7da..cfc24142d1ef 100644 --- a/code/core/src/csf-tools/ConfigFile.ts +++ b/code/core/src/csf-tools/ConfigFile.ts @@ -1,6 +1,5 @@ /* eslint-disable no-underscore-dangle */ import { readFile, writeFile } from 'node:fs/promises'; -import { readFileSync } from 'node:fs'; import { type RecastOptions, @@ -14,7 +13,6 @@ import { import { dedent } from 'ts-dedent'; import type { PrintResultType } from './PrintResultType'; -import path from 'node:path'; const logger = console; @@ -907,11 +905,6 @@ export const readConfig = async (fileName: string) => { return loadConfig(code, fileName).parse(); }; -export const readConfigSync = (fileName: string) => { - const code = readFileSync(fileName, 'utf-8').toString(); - return loadConfig(code).parse(); -}; - export const writeConfig = async (config: ConfigFile, fileName?: string) => { const fname = fileName || config.fileName; From 1bbb801d5e2b42ea6c3a74817840bdad46a995d9 Mon Sep 17 00:00:00 2001 From: vctqs1 Date: Sat, 12 Oct 2024 19:02:50 +0700 Subject: [PATCH 20/53] add step to check decl --- code/core/src/csf-tools/ConfigFile.test.ts | 1 - code/core/src/csf-tools/parameters.data.ts | 1 - code/vitest.config.ts | 1 - 3 files changed, 3 deletions(-) delete mode 100644 code/core/src/csf-tools/parameters.data.ts diff --git a/code/core/src/csf-tools/ConfigFile.test.ts b/code/core/src/csf-tools/ConfigFile.test.ts index 089cbaa8e6ea..44c6d09c6814 100644 --- a/code/core/src/csf-tools/ConfigFile.test.ts +++ b/code/core/src/csf-tools/ConfigFile.test.ts @@ -1331,7 +1331,6 @@ describe('ConfigFile', () => { `; const config = loadConfig(source).parse() - // ensure config._exportDecls vs config._exports has 'parameters' expect(config._exportDecls['path']).toBe(undefined) expect(config._exports['path']).toBe(undefined) }) diff --git a/code/core/src/csf-tools/parameters.data.ts b/code/core/src/csf-tools/parameters.data.ts deleted file mode 100644 index 93e5d6e28c78..000000000000 --- a/code/core/src/csf-tools/parameters.data.ts +++ /dev/null @@ -1 +0,0 @@ -export const parameters = {} \ No newline at end of file diff --git a/code/vitest.config.ts b/code/vitest.config.ts index b848d9cc58bb..cabfb4e8b0ce 100644 --- a/code/vitest.config.ts +++ b/code/vitest.config.ts @@ -2,7 +2,6 @@ import { coverageConfigDefaults, defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ["code/core/src/csf-tools/ConfigFile.test.ts"], coverage: { all: false, provider: 'istanbul', From 8d1fe916c93f757c319ad7d72be3646612ffa1fb Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sat, 12 Oct 2024 22:27:45 +0800 Subject: [PATCH 21/53] Fix check --- code/core/src/manager/components/sidebar/Search.stories.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/code/core/src/manager/components/sidebar/Search.stories.tsx b/code/core/src/manager/components/sidebar/Search.stories.tsx index 5c70863735e0..1638f06b4dea 100644 --- a/code/core/src/manager/components/sidebar/Search.stories.tsx +++ b/code/core/src/manager/components/sidebar/Search.stories.tsx @@ -47,11 +47,7 @@ const baseProps = { export const Simple: StoryFn = () => {() => null}; -export const SimpleWithCreateButton: StoryFn = () => ( - - {() => null} - -); +export const SimpleWithCreateButton: StoryFn = () => {() => null}; export const FilledIn: StoryFn = () => ( From 763e95ed3d286b97ece10e6f477b55fb88738a29 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sun, 13 Oct 2024 11:31:38 +0800 Subject: [PATCH 22/53] Add navigation options to applyQueryParams API --- code/core/src/manager-api/modules/url.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/core/src/manager-api/modules/url.ts b/code/core/src/manager-api/modules/url.ts index 40971e9dea42..1b08ba7358d4 100644 --- a/code/core/src/manager-api/modules/url.ts +++ b/code/core/src/manager-api/modules/url.ts @@ -159,9 +159,10 @@ export interface SubAPI { * Set the query parameters for the current URL & navigates. * * @param {QueryParams} input - An object containing the query parameters to set. + * @param {NavigateOptions} options - Options for the navigation. * @returns {void} */ - applyQueryParams: (input: QueryParams) => void; + applyQueryParams: (input: QueryParams, options?: NavigateOptions) => void; } export const init: ModuleFn = (moduleArgs) => { @@ -206,10 +207,10 @@ export const init: ModuleFn = (moduleArgs) => { provider.channel?.emit(UPDATE_QUERY_PARAMS, update); } }, - applyQueryParams(input) { + applyQueryParams(input, options) { const { path, queryParams } = api.getUrlState(); - navigateTo(path, { ...queryParams, ...input } as any); + navigateTo(path, { ...queryParams, ...input } as any, options); api.setQueryParams(input); }, navigateUrl(url, options) { From c7603f3d8cb1f437b3d7126435fd2ddb58b3afb2 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sun, 13 Oct 2024 11:31:56 +0800 Subject: [PATCH 23/53] Tags filter: Use built-in queryParams API --- .../src/manager/components/sidebar/Sidebar.tsx | 18 +----------------- .../manager/components/sidebar/TagsFilter.tsx | 13 ++++--------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index 8409e3ace435..6fd983dc2288 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -120,18 +120,6 @@ const useCombination = ( return useMemo(() => ({ hash, entries: Object.entries(hash) }), [hash]); }; -const updateQueryParams = (params: Record) => { - const url = new URL(window.location.href); - Object.entries(params).forEach(([key, value]) => { - if (value) { - url.searchParams.set(key, value); - } else { - url.searchParams.delete(key); - } - }); - window.history.pushState({}, '', url); -}; - const isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT'; const isRendererReact = global.STORYBOOK_RENDERER === 'react'; @@ -213,11 +201,7 @@ export const Sidebar = React.memo(function Sidebar({ ) } - searchFieldContent={ - indexJson && ( - - ) - } + searchFieldContent={indexJson && } {...lastViewedProps} > {({ diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index e7e7a4595d71..401103c2d154 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -35,16 +35,10 @@ const Count = styled(Badge)(({ theme }) => ({ export interface TagsFilterProps { api: API; indexJson: StoryIndex; - updateQueryParams: (params: Record) => void; initialSelectedTags?: Tag[]; } -export const TagsFilter = ({ - api, - indexJson, - updateQueryParams, - initialSelectedTags = [], -}: TagsFilterProps) => { +export const TagsFilter = ({ api, indexJson, initialSelectedTags = [] }: TagsFilterProps) => { const [selectedTags, setSelectedTags] = useState(initialSelectedTags); const [expanded, setExpanded] = useState(false); const tagsActive = selectedTags.length > 0; @@ -58,9 +52,10 @@ export const TagsFilter = ({ return selectedTags.some((tag) => item.tags?.includes(tag)); }); + const { url } = api.getUrlState(); const includeTags = selectedTags.join(','); - updateQueryParams({ includeTags }); - }, [api, selectedTags, updateQueryParams]); + api.applyQueryParams({ includeTags }, { replace: true }); + }, [api, selectedTags]); const allTags = Object.values(indexJson.entries).reduce((acc, entry) => { entry.tags?.forEach((tag: Tag) => acc.add(tag)); From 812f9d9ce08df9adb8e887144fe0766a8a82dd26 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Mon, 14 Oct 2024 14:59:06 +0800 Subject: [PATCH 24/53] Fix TagsFilter stories --- .../src/manager/components/sidebar/Sidebar.stories.tsx | 8 ++++++++ .../src/manager/components/sidebar/TagsFilter.stories.tsx | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index 77086b886cce..e4dd164f4ff0 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -43,6 +43,14 @@ const managerContext: any = { ), selectStory: fn().mockName('api::selectStory'), experimental_setFilter: fn().mockName('api::experimental_setFilter'), + getDocsUrl: () => 'https://storybook.js.org/docs/', + getUrlState: () => ({ + queryParams: {}, + path: '', + viewMode: 'story', + url: 'http://localhost:6006/', + }), + applyQueryParams: fn().mockName('api::applyQueryParams'), }, }; diff --git a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx index eb277f33b3da..b897d80a4cf7 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx @@ -10,8 +10,14 @@ const meta = { api: { experimental_setFilter: fn(), getDocsUrl: () => 'https://storybook.js.org/docs/', + getUrlState: () => ({ + queryParams: {}, + path: '', + viewMode: 'story', + url: 'http://localhost:6006/', + }), + applyQueryParams: fn().mockName('api::applyQueryParams'), } as any, - updateQueryParams: fn(), }, } satisfies Meta; From a088fc080c3fc98db1ac2ae4db13b82628afbd37 Mon Sep 17 00:00:00 2001 From: David Leuliette Date: Mon, 14 Oct 2024 11:27:34 +0200 Subject: [PATCH 25/53] docs: drop /react slug from addons reference --- code/addons/actions/ADVANCED.md | 2 +- code/addons/actions/README.md | 2 +- code/addons/backgrounds/README.md | 2 +- code/addons/controls/README.md | 6 +++--- code/addons/docs/docs/props-tables.md | 6 +++--- code/addons/toolbars/README.md | 4 ++-- code/addons/viewport/README.md | 2 +- code/core/src/preview-api/modules/store/inferArgTypes.ts | 4 ++-- code/lib/blocks/src/blocks/Canvas.stories.tsx | 2 +- code/lib/blocks/src/components/ArgsTable/ArgControl.tsx | 2 +- code/lib/blocks/src/examples/CanvasParameters.stories.tsx | 2 +- code/lib/test/src/testing-library.ts | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/code/addons/actions/ADVANCED.md b/code/addons/actions/ADVANCED.md index 7cca56342dee..d71320209288 100644 --- a/code/addons/actions/ADVANCED.md +++ b/code/addons/actions/ADVANCED.md @@ -1,6 +1,6 @@ ## Advanced/Legacy Actions usage -For basic usage, see the [documentation](https://storybook.js.org/docs/react/essentials/actions). +For basic usage, see the [documentation](https://storybook.js.org/docs/essentials/actions). This document describes the pre-6.0 usage of the addon, and as such is no longer recommended (although it will be supported until at least 7.0). diff --git a/code/addons/actions/README.md b/code/addons/actions/README.md index 80d2bd5d746c..a24f0e7052a2 100644 --- a/code/addons/actions/README.md +++ b/code/addons/actions/README.md @@ -24,4 +24,4 @@ export default { ## Usage -The basic usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/actions). For legacy usage, see the [advanced README](./ADVANCED.md). +The basic usage is documented in the [documentation](https://storybook.js.org/docs/essentials/actions). For legacy usage, see the [advanced README](./ADVANCED.md). diff --git a/code/addons/backgrounds/README.md b/code/addons/backgrounds/README.md index 481ba54bcd2f..a720847d4bf0 100644 --- a/code/addons/backgrounds/README.md +++ b/code/addons/backgrounds/README.md @@ -26,4 +26,4 @@ export default { ## Usage -The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/backgrounds). +The usage is documented in the [documentation](https://storybook.js.org/docs/essentials/backgrounds). diff --git a/code/addons/controls/README.md b/code/addons/controls/README.md index 192a112e07fe..b3c097e53eeb 100644 --- a/code/addons/controls/README.md +++ b/code/addons/controls/README.md @@ -24,7 +24,7 @@ export default { ## Usage -The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/controls). +The usage is documented in the [documentation](https://storybook.js.org/docs/essentials/controls). ## FAQs @@ -92,7 +92,7 @@ export const Reflow = () => { }; ``` -And again, as above, this can be rewritten using [fully custom args](https://storybook.js.org/docs/react/essentials/controls#fully-custom-args): +And again, as above, this can be rewritten using [fully custom args](https://storybook.js.org/docs/essentials/controls#fully-custom-args): ```jsx export const Reflow = ({ count, label, ...args }) => ( @@ -147,7 +147,7 @@ Basic.args = { }; ``` -The `argTypes` annotation (which can also be applied to individual stories if needed), gives Storybook the hints it needs to generate controls in these unsupported cases. See [control annotations](https://storybook.js.org/docs/react/essentials/controls#annotation) for a full list of control types. +The `argTypes` annotation (which can also be applied to individual stories if needed), gives Storybook the hints it needs to generate controls in these unsupported cases. See [control annotations](https://storybook.js.org/docs/essentials/controls#annotation) for a full list of control types. It's also possible that your Storybook is misconfigured. If you think this might be the case, please search through Storybook's [Github issues](https://github.com/storybookjs/storybook/issues), and file a new issue if you don't find one that matches your use case. diff --git a/code/addons/docs/docs/props-tables.md b/code/addons/docs/docs/props-tables.md index 7a44a6c653ae..6cb5029b00a6 100644 --- a/code/addons/docs/docs/props-tables.md +++ b/code/addons/docs/docs/props-tables.md @@ -82,7 +82,7 @@ export const WithControls = (args) => ; ``` -For a very detailed walkthrough of how to write stories that use controls, read the [documentation](https://storybook.js.org/docs/react/essentials/controls). +For a very detailed walkthrough of how to write stories that use controls, read the [documentation](https://storybook.js.org/docs/essentials/controls). ## Customization @@ -187,7 +187,7 @@ This would render a row with a modified description, a type display with a dropd > - `type: 'number'` is shorthand for `type: { name: 'number' }` > - `control: 'radio'` is shorthand for `control: { type: 'radio' }` -Controls customization has an entire section in the [documentation](https://storybook.js.org/docs/react/essentials/controls#configuration). +Controls customization has an entire section in the [documentation](https://storybook.js.org/docs/essentials/controls#configuration). Here are the possible customizations for the rest of the prop table: @@ -200,7 +200,7 @@ Here are the possible customizations for the rest of the prop table: | `table.type.detail` | A longer version of the type (if it's a complex type) | | `table.defaultValue.summary` | A short version of the default value | | `table.defaultValue.detail` | A longer version of the default value (if it's a complex value) | -| `control` | See [`addon-controls` README](https://storybook.js.org/docs/react/essentials/controls#configuration) | +| `control` | See [`addon-controls` README](https://storybook.js.org/docs/essentials/controls#configuration) | ## Reporting a bug diff --git a/code/addons/toolbars/README.md b/code/addons/toolbars/README.md index dcb9b9c4deef..2e37dc6c6168 100644 --- a/code/addons/toolbars/README.md +++ b/code/addons/toolbars/README.md @@ -28,7 +28,7 @@ export default { ## Usage -The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/toolbars-and-globals). +The usage is documented in the [documentation](https://storybook.js.org/docs/essentials/toolbars-and-globals). ## FAQs @@ -40,6 +40,6 @@ The primary difference between the two packages is that `addon-toolbars` makes u - **Standardization**. Args are built into Storybook in 6.x. Since `addon-toolbars` is based on args, you don't need to learn any addon-specific APIs to use it. -- **Ergonomics**. Global args are easy to consume [in stories](https://storybook.js.org/docs/react/essentials/toolbars-and-globals#consuming-globals-from-within-a-story), in [Storybook Docs](https://github.com/storybookjs/storybook/tree/next/code/addons/docs), or even in other addons. +- **Ergonomics**. Global args are easy to consume [in stories](https://storybook.js.org/docs/essentials/toolbars-and-globals#consuming-globals-from-within-a-story), in [Storybook Docs](https://github.com/storybookjs/storybook/tree/next/code/addons/docs), or even in other addons. * **Framework compatibility**. Args are completely framework-independent, so `addon-toolbars` is compatible with React, Vue 3, Angular, etc. out of the box with no framework logic needed in the addon. diff --git a/code/addons/viewport/README.md b/code/addons/viewport/README.md index 7975688745fd..6444f180db9f 100644 --- a/code/addons/viewport/README.md +++ b/code/addons/viewport/README.md @@ -26,4 +26,4 @@ You should now be able to see the viewport addon icon in the toolbar at the top ## Usage -The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/viewport). +The usage is documented in the [documentation](https://storybook.js.org/docs/essentials/viewport). diff --git a/code/core/src/preview-api/modules/store/inferArgTypes.ts b/code/core/src/preview-api/modules/store/inferArgTypes.ts index 007b4971a173..35e840fedcd8 100644 --- a/code/core/src/preview-api/modules/store/inferArgTypes.ts +++ b/code/core/src/preview-api/modules/store/inferArgTypes.ts @@ -25,8 +25,8 @@ const inferType = (value: any, name: string, visited: Set): SBType => { We've detected a cycle in arg '${name}'. Args should be JSON-serializable. Consider using the mapping feature or fully custom args: - - Mapping: https://storybook.js.org/docs/react/writing-stories/args#mapping-to-complex-arg-values - - Custom args: https://storybook.js.org/docs/react/essentials/controls#fully-custom-args + - Mapping: https://storybook.js.org/docs/writing-stories/args#mapping-to-complex-arg-values + - Custom args: https://storybook.js.org/docs/essentials/controls#fully-custom-args `); return { name: 'other', value: 'cyclic object' }; } diff --git a/code/lib/blocks/src/blocks/Canvas.stories.tsx b/code/lib/blocks/src/blocks/Canvas.stories.tsx index 4a7e25f6706d..bdedf650e534 100644 --- a/code/lib/blocks/src/blocks/Canvas.stories.tsx +++ b/code/lib/blocks/src/blocks/Canvas.stories.tsx @@ -94,7 +94,7 @@ export const PropAdditionalActions: Story = { title: 'Go to documentation', onClick: () => { window.open( - 'https://storybook.js.org/docs/react/essentials/controls#annotation', + 'https://storybook.js.org/docs/essentials/controls#annotation', '_blank' ); }, diff --git a/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx b/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx index 36e75aa81d36..e90609eb51c2 100644 --- a/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx +++ b/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx @@ -72,7 +72,7 @@ export const ArgControl: FC = ({ row, arg, updateArgs, isHovere const canBeSetup = control?.disable !== true && row?.type?.name !== 'function'; return isHovered && canBeSetup ? ( diff --git a/code/lib/blocks/src/examples/CanvasParameters.stories.tsx b/code/lib/blocks/src/examples/CanvasParameters.stories.tsx index aa9affefbfec..6f0c3cb3484c 100644 --- a/code/lib/blocks/src/examples/CanvasParameters.stories.tsx +++ b/code/lib/blocks/src/examples/CanvasParameters.stories.tsx @@ -31,7 +31,7 @@ export const AdditionalActions: Story = { title: 'Go to documentation', onClick: () => { window.open( - 'https://storybook.js.org/docs/react/essentials/controls#annotation', + 'https://storybook.js.org/docs/essentials/controls#annotation', '_blank' ); }, diff --git a/code/lib/test/src/testing-library.ts b/code/lib/test/src/testing-library.ts index 1aa814dafdce..4ff01e47eacf 100644 --- a/code/lib/test/src/testing-library.ts +++ b/code/lib/test/src/testing-library.ts @@ -28,7 +28,7 @@ testingLibrary.screen = new Proxy(testingLibrary.screen, { get(target, prop, receiver) { once.warn(dedent` You are using Testing Library's \`screen\` object. Use \`within(canvasElement)\` instead. - More info: https://storybook.js.org/docs/react/essentials/interactions + More info: https://storybook.js.org/docs/essentials/interactions `); return Reflect.get(target, prop, receiver); }, From 4b3fc7b532a2c85cdb1ba7f7b06c6b098d29a385 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Wed, 16 Oct 2024 00:17:30 +0800 Subject: [PATCH 26/53] Fix linting --- code/core/src/csf-tools/ConfigFile.test.ts | 21 +++++++-------------- code/core/src/csf-tools/ConfigFile.ts | 5 ++--- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/code/core/src/csf-tools/ConfigFile.test.ts b/code/core/src/csf-tools/ConfigFile.test.ts index 44c6d09c6814..e3409d5eb681 100644 --- a/code/core/src/csf-tools/ConfigFile.test.ts +++ b/code/core/src/csf-tools/ConfigFile.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ import { describe, expect, it } from 'vitest'; import { babelPrint } from '@storybook/core/babel'; @@ -1080,7 +1081,6 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse(); config.setImport('path', 'path'); - // eslint-disable-next-line no-underscore-dangle const parsed = babelPrint(config._ast); expect(parsed).toMatchInlineSnapshot(` @@ -1099,7 +1099,6 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse(); config.setImport('path', 'path'); - // eslint-disable-next-line no-underscore-dangle const parsed = babelPrint(config._ast); expect(parsed).toMatchInlineSnapshot(` @@ -1118,7 +1117,6 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse(); config.setImport(['dirname'], 'path'); - // eslint-disable-next-line no-underscore-dangle const parsed = babelPrint(config._ast); expect(parsed).toMatchInlineSnapshot(` @@ -1139,7 +1137,6 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse(); config.setImport(['dirname'], 'path'); - // eslint-disable-next-line no-underscore-dangle const parsed = babelPrint(config._ast); expect(parsed).toMatchInlineSnapshot(` @@ -1161,7 +1158,6 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse(); config.setRequireImport('path', 'path'); - // eslint-disable-next-line no-underscore-dangle const parsed = babelPrint(config._ast); expect(parsed).toMatchInlineSnapshot(` @@ -1181,7 +1177,6 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse(); config.setRequireImport('path', 'path'); - // eslint-disable-next-line no-underscore-dangle const parsed = babelPrint(config._ast); expect(parsed).toMatchInlineSnapshot(` @@ -1200,7 +1195,6 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse(); config.setRequireImport(['dirname'], 'path'); - // eslint-disable-next-line no-underscore-dangle const parsed = babelPrint(config._ast); expect(parsed).toMatchInlineSnapshot(` @@ -1224,7 +1218,6 @@ describe('ConfigFile', () => { const config = loadConfig(source).parse(); config.setRequireImport(['dirname', 'basename'], 'path'); - // eslint-disable-next-line no-underscore-dangle const parsed = babelPrint(config._ast); expect(parsed).toMatchInlineSnapshot(` @@ -1311,7 +1304,7 @@ describe('ConfigFile', () => { describe('parse', () => { it("export { X } with X is import { X } from 'another-file'", () => { - const source = dedent` + const source = dedent` import type { StorybookConfig } from '@storybook/react-webpack5'; import { path } from 'path'; @@ -1329,10 +1322,10 @@ describe('ConfigFile', () => { } export default config; `; - const config = loadConfig(source).parse() + const config = loadConfig(source).parse(); - expect(config._exportDecls['path']).toBe(undefined) - expect(config._exports['path']).toBe(undefined) - }) - }) + expect(config._exportDecls['path']).toBe(undefined); + expect(config._exports['path']).toBe(undefined); + }); + }); }); diff --git a/code/core/src/csf-tools/ConfigFile.ts b/code/core/src/csf-tools/ConfigFile.ts index cfc24142d1ef..dc9f973d5ad6 100644 --- a/code/core/src/csf-tools/ConfigFile.ts +++ b/code/core/src/csf-tools/ConfigFile.ts @@ -102,7 +102,7 @@ const _findVarDeclarator = ( ): t.VariableDeclarator | null | undefined => { let declarator: t.VariableDeclarator | null | undefined = null; let declarations: t.VariableDeclarator[] | null = null; - + program.body.find((node: t.Node) => { if (t.isVariableDeclaration(node)) { declarations = node.declarations; @@ -252,11 +252,10 @@ export class ConfigFile { const decl = _findVarDeclarator(localName, parent as t.Program) as any; // decl can be empty in case X from `import { X } from ....` because it is not handled in _findVarDeclarator - if(decl) { + if (decl) { self._exports[exportName] = decl.init; self._exportDecls[exportName] = decl; } - } }); } else { From b769cd73dc8a1bab4c1378e50719129ff771a41c Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Wed, 16 Oct 2024 00:48:14 +0800 Subject: [PATCH 27/53] Fix lint --- code/lib/blocks/src/blocks/Canvas.stories.tsx | 5 +---- code/lib/blocks/src/components/ArgsTable/ArgControl.tsx | 6 +----- code/lib/blocks/src/examples/CanvasParameters.stories.tsx | 5 +---- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/code/lib/blocks/src/blocks/Canvas.stories.tsx b/code/lib/blocks/src/blocks/Canvas.stories.tsx index bdedf650e534..987e28678a71 100644 --- a/code/lib/blocks/src/blocks/Canvas.stories.tsx +++ b/code/lib/blocks/src/blocks/Canvas.stories.tsx @@ -93,10 +93,7 @@ export const PropAdditionalActions: Story = { { title: 'Go to documentation', onClick: () => { - window.open( - 'https://storybook.js.org/docs/essentials/controls#annotation', - '_blank' - ); + window.open('https://storybook.js.org/docs/essentials/controls#annotation', '_blank'); }, }, ], diff --git a/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx b/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx index e90609eb51c2..064a115dd410 100644 --- a/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx +++ b/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx @@ -71,11 +71,7 @@ export const ArgControl: FC = ({ row, arg, updateArgs, isHovere if (!control || control.disable) { const canBeSetup = control?.disable !== true && row?.type?.name !== 'function'; return isHovered && canBeSetup ? ( - + Setup controls ) : ( diff --git a/code/lib/blocks/src/examples/CanvasParameters.stories.tsx b/code/lib/blocks/src/examples/CanvasParameters.stories.tsx index 6f0c3cb3484c..4d4433747494 100644 --- a/code/lib/blocks/src/examples/CanvasParameters.stories.tsx +++ b/code/lib/blocks/src/examples/CanvasParameters.stories.tsx @@ -30,10 +30,7 @@ export const AdditionalActions: Story = { { title: 'Go to documentation', onClick: () => { - window.open( - 'https://storybook.js.org/docs/essentials/controls#annotation', - '_blank' - ); + window.open('https://storybook.js.org/docs/essentials/controls#annotation', '_blank'); }, }, ], From ceb32c2aec3176f72f3de559ac11f5ccbd2b27dd Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 16 Oct 2024 12:39:07 +0200 Subject: [PATCH 28/53] simplify cleaning outDir on static build --- code/core/src/core-server/build-static.ts | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/code/core/src/core-server/build-static.ts b/code/core/src/core-server/build-static.ts index 8d8d5f9cce60..ab0d979acaf5 100644 --- a/code/core/src/core-server/build-static.ts +++ b/code/core/src/core-server/build-static.ts @@ -47,22 +47,8 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption if (options.outputDir === '/') { throw new Error("Won't remove directory '/'. Check your outputDir!"); } - - try { - const outputDirFiles = await readdir(options.outputDir); - for (const file of outputDirFiles) { - await rm(file, { recursive: true, force: true }); - } - } catch { - await mkdir(options.outputDir, { recursive: true }); - } - - if (!existsSync(options.outputDir)) { - await mkdir(options.outputDir, { recursive: true }); - } else if ((await readdir(options.outputDir)).length > 0) { - await rm(options.outputDir, { recursive: true, force: true }); - await mkdir(options.outputDir, { recursive: true }); - } + await rm(options.outputDir, { recursive: true, force: true }).catch(() => {}); + await mkdir(options.outputDir, { recursive: true }); const config = await loadMainConfig(options); const { framework } = config; From c48fd2f249a5f7eccab04f1adaadccc0a10c88ce Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Wed, 16 Oct 2024 22:44:57 +0800 Subject: [PATCH 29/53] Add custom tag filtering docs --- docs/writing-stories/tags.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/writing-stories/tags.mdx b/docs/writing-stories/tags.mdx index 25459a84ba59..a56e39b97fdc 100644 --- a/docs/writing-stories/tags.mdx +++ b/docs/writing-stories/tags.mdx @@ -51,6 +51,21 @@ To remove a tag from a story, prefix it with `!`. For example: Tags can be removed for all stories in your project (in `.storybook/preview.js|ts`), all stories for a component (in the CSF file meta), or a single story (as above). +## Filtering by custom tags + +Custom tags enable a flexible layer of categorization on top of Storybook's sidebar hierarchy. In the example above, we created an `experimental` tag to indicate that a story is not yet stable. + +You can create custom tags for any purpose. Sample uses might include: +- Status such as `experimental`, `new`, `stable`, or `deprecated` +- User persona such as `admin`, `user`, or `developer` +- Component ownerhip + +Custom tags are useful because they show up as filters in Storybook's sidebar. Selecting a tag in the filter causes the sidebar to only show stories with that tag. Selecting multiple tags shows stories that contain any of those tags. + +
); From 3eb65af3c5c03b38404c5b6bb1eba2660ab821be Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 16 Oct 2024 22:12:56 +0200 Subject: [PATCH 32/53] add svelte 5 story templates to Svelte renderer --- .../cli/svelte-5-js/Button.stories.svelte | 31 ++++++++ .../template/cli/svelte-5-js/Button.svelte | 26 +++++++ .../cli/svelte-5-js/Header.stories.svelte | 26 +++++++ .../template/cli/svelte-5-js/Header.svelte | 47 +++++++++++++ .../cli/svelte-5-js/Page.stories.svelte | 30 ++++++++ .../template/cli/svelte-5-js/Page.svelte | 70 +++++++++++++++++++ .../cli/svelte-5-ts-3-8/Button.stories.svelte | 31 ++++++++ .../cli/svelte-5-ts-3-8/Button.svelte | 29 ++++++++ .../cli/svelte-5-ts-3-8/Header.stories.svelte | 26 +++++++ .../cli/svelte-5-ts-3-8/Header.svelte | 45 ++++++++++++ .../cli/svelte-5-ts-3-8/Page.stories.svelte | 30 ++++++++ .../template/cli/svelte-5-ts-3-8/Page.svelte | 70 +++++++++++++++++++ .../cli/svelte-5-ts-4-9/Button.stories.svelte | 31 ++++++++ .../cli/svelte-5-ts-4-9/Button.svelte | 29 ++++++++ .../cli/svelte-5-ts-4-9/Header.stories.svelte | 26 +++++++ .../cli/svelte-5-ts-4-9/Header.svelte | 45 ++++++++++++ .../cli/svelte-5-ts-4-9/Page.stories.svelte | 30 ++++++++ .../template/cli/svelte-5-ts-4-9/Page.svelte | 70 +++++++++++++++++++ 18 files changed, 692 insertions(+) create mode 100644 code/renderers/svelte/template/cli/svelte-5-js/Button.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-js/Button.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-js/Header.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-js/Header.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-js/Page.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-js/Page.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.stories.svelte create mode 100644 code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.svelte diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Button.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Button.stories.svelte new file mode 100644 index 000000000000..4c8c7cce632a --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Button.stories.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Button.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Button.svelte new file mode 100644 index 000000000000..b2b820ea4971 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Button.svelte @@ -0,0 +1,26 @@ + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Header.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Header.stories.svelte new file mode 100644 index 000000000000..0130c115acf6 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Header.stories.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Header.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Header.svelte new file mode 100644 index 000000000000..dba3b7880f49 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Header.svelte @@ -0,0 +1,47 @@ + + +
+
+
+ + + + + + + +

Acme

+
+
+ {#if user} + + Welcome, {user.name}! + +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Page.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Page.stories.svelte new file mode 100644 index 000000000000..aa7372c58ef4 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Page.stories.svelte @@ -0,0 +1,30 @@ + + + { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await waitFor(() => expect(loginButton).not.toBeInTheDocument()); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }} + /> + + diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Page.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Page.svelte new file mode 100644 index 000000000000..92a95c00c5c5 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Page.svelte @@ -0,0 +1,70 @@ + + +
+
(user = { name: 'Jane Doe' })} + onLogout={() => (user = null)} + onCreateAccount={() => (user = { name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a + + component-driven + + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page data + in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at + + Storybook tutorials + + . Read more in the + docs + . +

+
+ Tip + Adjust the width of the canvas with the + + + + + + Viewports addon in the toolbar +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.stories.svelte new file mode 100644 index 000000000000..4c8c7cce632a --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.stories.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.svelte new file mode 100644 index 000000000000..b31f5bffe4a5 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.svelte @@ -0,0 +1,29 @@ + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.stories.svelte new file mode 100644 index 000000000000..0130c115acf6 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.stories.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.svelte new file mode 100644 index 000000000000..14e890c79e98 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.svelte @@ -0,0 +1,45 @@ + + +
+
+
+ + + + + + + +

Acme

+
+
+ {#if user} + + Welcome, {user.name}! + +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.stories.svelte new file mode 100644 index 000000000000..ed850d83718e --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.stories.svelte @@ -0,0 +1,30 @@ + + + { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await waitFor(() => expect(loginButton).not.toBeInTheDocument()); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }} +/> + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.svelte new file mode 100644 index 000000000000..c4c069a5a50b --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.svelte @@ -0,0 +1,70 @@ + + +
+
(user = { name: 'Jane Doe' })} + onLogout={() => (user = undefined)} + onCreateAccount={() => (user = { name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a + + component-driven + + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page data + in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at + + Storybook tutorials + + . Read more in the + docs + . +

+
+ Tip + Adjust the width of the canvas with the + + + + + + Viewports addon in the toolbar +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.stories.svelte new file mode 100644 index 000000000000..4c8c7cce632a --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.stories.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.svelte new file mode 100644 index 000000000000..b31f5bffe4a5 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.svelte @@ -0,0 +1,29 @@ + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.stories.svelte new file mode 100644 index 000000000000..0130c115acf6 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.stories.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.svelte new file mode 100644 index 000000000000..14e890c79e98 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.svelte @@ -0,0 +1,45 @@ + + +
+
+
+ + + + + + + +

Acme

+
+
+ {#if user} + + Welcome, {user.name}! + +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.stories.svelte new file mode 100644 index 000000000000..ed850d83718e --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.stories.svelte @@ -0,0 +1,30 @@ + + + { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await waitFor(() => expect(loginButton).not.toBeInTheDocument()); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }} +/> + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.svelte new file mode 100644 index 000000000000..c4c069a5a50b --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.svelte @@ -0,0 +1,70 @@ + + +
+
(user = { name: 'Jane Doe' })} + onLogout={() => (user = undefined)} + onCreateAccount={() => (user = { name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a + + component-driven + + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page data + in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at + + Storybook tutorials + + . Read more in the + docs + . +

+
+ Tip + Adjust the width of the canvas with the + + + + + + Viewports addon in the toolbar +
+
+
From c764db191540b86784edf76508718a8591981aca Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 16 Oct 2024 22:13:09 +0200 Subject: [PATCH 33/53] upgrade Svelte to latest prerelease --- code/package.json | 2 +- code/yarn.lock | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/code/package.json b/code/package.json index bd2b064fce56..6f49f25d7ead 100644 --- a/code/package.json +++ b/code/package.json @@ -215,7 +215,7 @@ "serve-static": "^1.14.1", "slash": "^5.0.0", "storybook": "workspace:^", - "svelte": "^5.0.0-next.65", + "svelte": "^5.0.0-next.268", "ts-dedent": "^2.0.0", "typescript": "^5.4.3", "util": "^0.12.4", diff --git a/code/yarn.lock b/code/yarn.lock index 79822b6950b8..e5e350d27780 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6925,7 +6925,7 @@ __metadata: serve-static: "npm:^1.14.1" slash: "npm:^5.0.0" storybook: "workspace:^" - svelte: "npm:^5.0.0-next.65" + svelte: "npm:^5.0.0-next.268" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.4.3" util: "npm:^0.12.4" @@ -9926,6 +9926,13 @@ __metadata: languageName: node linkType: hard +"aria-query@npm:^5.3.1": + version: 5.3.2 + resolution: "aria-query@npm:5.3.2" + checksum: 10c0/003c7e3e2cff5540bf7a7893775fc614de82b0c5dde8ae823d47b7a28a9d4da1f7ed85f340bdb93d5649caa927755f0e31ecc7ab63edfdfc00c8ef07e505e03e + languageName: node + linkType: hard + "arr-diff@npm:^4.0.0": version: 4.0.0 resolution: "arr-diff@npm:4.0.0" @@ -10337,6 +10344,13 @@ __metadata: languageName: node linkType: hard +"axobject-query@npm:^4.1.0": + version: 4.1.0 + resolution: "axobject-query@npm:4.1.0" + checksum: 10c0/c470e4f95008f232eadd755b018cb55f16c03ccf39c027b941cd8820ac6b68707ce5d7368a46756db4256fbc91bb4ead368f84f7fb034b2b7932f082f6dc0775 + languageName: node + linkType: hard + "babel-core@npm:^7.0.0-bridge.0": version: 7.0.0-bridge.0 resolution: "babel-core@npm:7.0.0-bridge.0" @@ -14727,6 +14741,16 @@ __metadata: languageName: node linkType: hard +"esrap@npm:^1.2.2": + version: 1.2.2 + resolution: "esrap@npm:1.2.2" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.4.15" + "@types/estree": "npm:^1.0.1" + checksum: 10c0/a3a0b665c034f604a162b910346309c64c42635c5d2e8704a27afcdf4e6d4c529e05475d1875d6b3e0d550f8470986116414097230ab3a7c565b85091ca5e177 + languageName: node + linkType: hard + "esrecurse@npm:^4.3.0": version: 4.3.0 resolution: "esrecurse@npm:4.3.0" @@ -26752,6 +26776,27 @@ __metadata: languageName: node linkType: hard +"svelte@npm:^5.0.0-next.268": + version: 5.0.0-next.268 + resolution: "svelte@npm:5.0.0-next.268" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + "@types/estree": "npm:^1.0.5" + acorn: "npm:^8.12.1" + acorn-typescript: "npm:^1.4.13" + aria-query: "npm:^5.3.1" + axobject-query: "npm:^4.1.0" + esm-env: "npm:^1.0.0" + esrap: "npm:^1.2.2" + is-reference: "npm:^3.0.2" + locate-character: "npm:^3.0.0" + magic-string: "npm:^0.30.11" + zimmerframe: "npm:^1.1.2" + checksum: 10c0/74a954cffe2a70259a1d1d2a834e9615d3f393429ac8cc1e15bfdc66b8bbe5dc449a8289370631b29023bca51aa451d1906f570b3761de4c235ea731913ee1b2 + languageName: node + linkType: hard + "svelte@npm:^5.0.0-next.65": version: 5.0.0-next.65 resolution: "svelte@npm:5.0.0-next.65" From cc837c4ee97c80104067dec3413e3256a3d507b4 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 17 Oct 2024 08:29:11 +0800 Subject: [PATCH 34/53] Remove border for now --- code/core/src/manager/components/sidebar/TagsFilter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index d177111d30a5..5e2823bc9597 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -27,7 +27,6 @@ const TagSelected = styled(Badge)(({ theme }) => ({ height: 6, minWidth: 6, lineHeight: 'px', - border: `2px solid`, boxShadow: `${theme.barSelectedColor} 0 0 0 1px inset`, fontSize: theme.typography.size.s1 - 1, background: theme.color.secondary, From 0861679101ff64f5c7075f78c2c435ba5346d34c Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 17 Oct 2024 12:17:24 +0800 Subject: [PATCH 35/53] CLI: Use Svelte 5 CSF template files --- code/core/src/cli/helpers.ts | 33 +++++++++++++++++-- .../src/generators/SVELTE/index.ts | 32 ++++++------------ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index 38cf6151294f..4be9a584b47a 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -13,7 +13,7 @@ import type { SupportedFrameworks, SupportedRenderers } from '@storybook/core/ty import { findUpSync } from 'find-up'; import picocolors from 'picocolors'; -import { coerce, satisfies } from 'semver'; +import { coerce, major, satisfies } from 'semver'; import stripJsonComments from 'strip-json-comments'; import invariant from 'tiny-invariant'; @@ -173,6 +173,22 @@ export const frameworkToDefaultBuilder: Record< 'vue3-rsbuild': CommunityBuilder.Rsbuild, }; +export async function getVersionSafe(packageManager: JsPackageManager, packageName: string) { + try { + const version = await packageManager.getInstalledVersion(packageName); + if (version) { + return coerce(version); + } else { + const deps = await packageManager.getAllDependencies(); + const versionSpecifier = deps[packageName]; + return coerce(versionSpecifier); + } + } catch (err) { + // fall back to no version + } + return undefined; +} + export async function copyTemplateFiles({ packageManager, renderer, @@ -180,13 +196,26 @@ export async function copyTemplateFiles({ destination, commonAssetsDir, }: CopyTemplateFilesOptions) { - const languageFolderMapping: Record = { + let languageFolderMapping: Record = { // keeping this for backwards compatibility in case community packages are using it typescript: 'ts', [SupportedLanguage.JAVASCRIPT]: 'js', [SupportedLanguage.TYPESCRIPT_3_8]: 'ts-3-8', [SupportedLanguage.TYPESCRIPT_4_9]: 'ts-4-9', }; + // FIXME: remove after 9.0 + if (renderer === 'svelte') { + const svelteVersion = await getVersionSafe(packageManager, 'svelte'); + if (svelteVersion && major(svelteVersion) >= 5) { + languageFolderMapping = { + // keeping this for backwards compatibility in case community packages are using it + typescript: 'ts', + [SupportedLanguage.JAVASCRIPT]: 'svelte-5-js', + [SupportedLanguage.TYPESCRIPT_3_8]: 'svelte-5-ts-3-8', + [SupportedLanguage.TYPESCRIPT_4_9]: 'svelte-5-ts-4-9', + }; + } + } const templatePath = async () => { const baseDir = await getRendererDir(packageManager, renderer); const assetsDir = join(baseDir, 'template', 'cli'); diff --git a/code/lib/create-storybook/src/generators/SVELTE/index.ts b/code/lib/create-storybook/src/generators/SVELTE/index.ts index 21661f5b02b9..9d00099437fb 100644 --- a/code/lib/create-storybook/src/generators/SVELTE/index.ts +++ b/code/lib/create-storybook/src/generators/SVELTE/index.ts @@ -1,33 +1,21 @@ +import { getVersionSafe } from 'storybook/internal/cli'; import type { JsPackageManager } from 'storybook/internal/common'; -import { coerce, major } from 'semver'; +import { major } from 'semver'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; -const versionHelper = (svelteMajor?: number) => { - if (svelteMajor === 4) { - return '4'; - } - // TODO: update when addon-svelte-csf v5 is released - if (svelteMajor === 5) { - return '^5.0.0-next.0'; - } - return ''; -}; - export const getAddonSvelteCsfVersion = async (packageManager: JsPackageManager) => { - const svelteVersion = await packageManager.getInstalledVersion('svelte'); + const svelteVersion = await getVersionSafe(packageManager, 'svelte'); try { - if (svelteVersion) { - return versionHelper(major(coerce(svelteVersion) || '')); - } else { - const deps = await packageManager.getAllDependencies(); - const svelteSpecifier = deps['svelte']; - const coerced = coerce(svelteSpecifier); - if (coerced?.version) { - return versionHelper(major(coerced.version)); - } + const svelteMajor = major(svelteVersion ?? ''); + if (svelteMajor === 4) { + return '4'; + } + // TODO: update when addon-svelte-csf v5 is released + if (svelteMajor === 5) { + return '^5.0.0-next.0'; } } catch { // fallback to latest version From 8b5972fc186a91deab666fd00a27d0aefc0a163c Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 17 Oct 2024 12:33:31 +0800 Subject: [PATCH 36/53] Fix linting --- code/lib/blocks/src/blocks/Canvas.stories.tsx | 5 +---- code/lib/blocks/src/components/ArgsTable/ArgControl.tsx | 6 +----- code/lib/blocks/src/examples/CanvasParameters.stories.tsx | 5 +---- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/code/lib/blocks/src/blocks/Canvas.stories.tsx b/code/lib/blocks/src/blocks/Canvas.stories.tsx index bdedf650e534..987e28678a71 100644 --- a/code/lib/blocks/src/blocks/Canvas.stories.tsx +++ b/code/lib/blocks/src/blocks/Canvas.stories.tsx @@ -93,10 +93,7 @@ export const PropAdditionalActions: Story = { { title: 'Go to documentation', onClick: () => { - window.open( - 'https://storybook.js.org/docs/essentials/controls#annotation', - '_blank' - ); + window.open('https://storybook.js.org/docs/essentials/controls#annotation', '_blank'); }, }, ], diff --git a/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx b/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx index e90609eb51c2..064a115dd410 100644 --- a/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx +++ b/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx @@ -71,11 +71,7 @@ export const ArgControl: FC = ({ row, arg, updateArgs, isHovere if (!control || control.disable) { const canBeSetup = control?.disable !== true && row?.type?.name !== 'function'; return isHovered && canBeSetup ? ( - + Setup controls ) : ( diff --git a/code/lib/blocks/src/examples/CanvasParameters.stories.tsx b/code/lib/blocks/src/examples/CanvasParameters.stories.tsx index 6f0c3cb3484c..4d4433747494 100644 --- a/code/lib/blocks/src/examples/CanvasParameters.stories.tsx +++ b/code/lib/blocks/src/examples/CanvasParameters.stories.tsx @@ -30,10 +30,7 @@ export const AdditionalActions: Story = { { title: 'Go to documentation', onClick: () => { - window.open( - 'https://storybook.js.org/docs/essentials/controls#annotation', - '_blank' - ); + window.open('https://storybook.js.org/docs/essentials/controls#annotation', '_blank'); }, }, ], From f608e2240dfd11c6ef29c6b05adfa3533a920752 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 17 Oct 2024 13:03:07 +0800 Subject: [PATCH 37/53] Clean up helper and add tests --- code/core/src/cli/helpers.test.ts | 41 +++++++++++++++++++++++++++++++ code/core/src/cli/helpers.ts | 14 +++++++---- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/code/core/src/cli/helpers.test.ts b/code/core/src/cli/helpers.test.ts index 4615f0cfdd84..03e85ccc941d 100644 --- a/code/core/src/cli/helpers.test.ts +++ b/code/core/src/cli/helpers.test.ts @@ -75,6 +75,47 @@ describe('Helpers', () => { vi.clearAllMocks(); }); + describe('getVersionSafe', () => { + describe('installed', () => { + it.each([ + ['3.0.0', '3.0.0'], + ['5.0.0-next.0', '5.0.0-next.0'], + [ + '4.2.19::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsvelte%2F-%2Fsvelte-4.2.19.tgz', + '4.2.19', + ], + ])('svelte %s => %s', async (svelteVersion, expectedAddonSpecifier) => { + const packageManager = { + getInstalledVersion: async (pkg: string) => + pkg === 'svelte' ? svelteVersion : undefined, + getAllDependencies: async () => ({ svelte: `^${svelteVersion}` }), + } as any as JsPackageManager; + await expect(helpers.getVersionSafe(packageManager, 'svelte')).resolves.toBe( + expectedAddonSpecifier + ); + }); + }); + + describe('uninstalled', () => { + it.each([ + ['^3', '3.0.0'], + ['^5.0.0-next.0', '5.0.0-next.0'], + [ + '4.2.19::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsvelte%2F-%2Fsvelte-4.2.19.tgz', + '4.2.19', + ], + ])('svelte %s => %s', async (svelteSpecifier, expectedAddonSpecifier) => { + const packageManager = { + getInstalledVersion: async (pkg: string) => undefined, + getAllDependencies: async () => ({ svelte: svelteSpecifier }), + } as any as JsPackageManager; + await expect(helpers.getVersionSafe(packageManager, 'svelte')).resolves.toBe( + expectedAddonSpecifier + ); + }); + }); + }); + describe('copyTemplate', () => { it(`should copy template files when directory is present`, () => { const csfDirectory = /template-csf\/$/; diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index 4be9a584b47a..47df29aba89e 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -173,16 +173,20 @@ export const frameworkToDefaultBuilder: Record< 'vue3-rsbuild': CommunityBuilder.Rsbuild, }; +/** + * Return the installed version of a package, or the coerced version specifier from package.json if + * it's a dependency but not installed (e.g. in a fresh project) + */ export async function getVersionSafe(packageManager: JsPackageManager, packageName: string) { try { - const version = await packageManager.getInstalledVersion(packageName); - if (version) { - return coerce(version); - } else { + let version = await packageManager.getInstalledVersion(packageName); + if (!version) { const deps = await packageManager.getAllDependencies(); const versionSpecifier = deps[packageName]; - return coerce(versionSpecifier); + version = versionSpecifier ?? ''; } + const coerced = coerce(version, { includePrerelease: true }); + return coerced?.toString(); } catch (err) { // fall back to no version } From f1b65b04efc039dbf03b52a5a2c6e626567825b7 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Thu, 17 Oct 2024 15:39:51 +0200 Subject: [PATCH 38/53] Fix Svelte type issues --- code/frameworks/svelte-vite/package.json | 2 +- code/renderers/svelte/package.json | 4 +- .../__test__/composeStories/Button.stories.ts | 34 +++--- code/yarn.lock | 106 ++++++++---------- 4 files changed, 69 insertions(+), 77 deletions(-) diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index 0fa571b8cb0f..bc84fb925c87 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -57,7 +57,7 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.1", "@types/node": "^22.0.0", - "svelte": "^5.0.0-next.65", + "svelte": "^5.0.0-next.268", "typescript": "^5.3.2", "vite": "^4.0.0" }, diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json index db0cd01b454b..d2fd3a847f7a 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -68,8 +68,8 @@ "@sveltejs/vite-plugin-svelte": "^3.0.2", "@testing-library/svelte": "patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch", "expect-type": "^0.15.0", - "svelte": "^5.0.0-next.65", - "svelte-check": "^3.6.4", + "svelte": "^5.0.0-next.268", + "svelte-check": "^4.0.5", "typescript": "^5.3.2" }, "peerDependencies": { diff --git a/code/renderers/svelte/src/__test__/composeStories/Button.stories.ts b/code/renderers/svelte/src/__test__/composeStories/Button.stories.ts index 94558abf996e..92ade46f29a8 100644 --- a/code/renderers/svelte/src/__test__/composeStories/Button.stories.ts +++ b/code/renderers/svelte/src/__test__/composeStories/Button.stories.ts @@ -53,7 +53,11 @@ const getCaptionForLocale = (locale: string) => { } }; -export const CSF2StoryWithLocale: CSF2Story = (args, { globals }) => ({ +// @ts-expect-error -- incompatibility with Svelte 5 types and CSF +export const CSF2StoryWithLocale: CSF2Story = ( + args, + { globals } +) => ({ Component: StoryWithLocaleComponent, props: { ...args, @@ -113,10 +117,11 @@ export const CSF3ButtonWithRender: StoryObj = { }), }; -export const CSF3InputFieldFilled: StoryObj = { - render: () => ({ - Component: InputFilledStoryComponent, - }), +export const CSF3InputFieldFilled: StoryObj = { + render: () => + ({ + Component: InputFilledStoryComponent, + }) as any, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Step label', async () => { @@ -128,10 +133,10 @@ export const CSF3InputFieldFilled: StoryObj = { }; const mockFn = fn(); -export const LoaderStory: StoryObj = { +export const LoaderStory: StoryObj = { args: { mockFn, - }, + } as any, loaders: [ async () => { mockFn.mockReturnValueOnce('mockFn return value'); @@ -140,13 +145,14 @@ export const LoaderStory: StoryObj = { }; }, ], - render: (args, { loaded }) => ({ - Component: LoaderStoryComponent, - props: { - ...args, - loaded, - }, - }), + render: (args, { loaded }) => + ({ + Component: LoaderStoryComponent, + props: { + ...args, + loaded, + }, + }) as any, play: async () => { expect(mockFn).toHaveBeenCalledWith('render'); }, diff --git a/code/yarn.lock b/code/yarn.lock index ddc9e33ef86f..a81df9a4ec6e 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6998,7 +6998,7 @@ __metadata: "@sveltejs/vite-plugin-svelte": "npm:^3.0.1" "@types/node": "npm:^22.0.0" magic-string: "npm:^0.30.0" - svelte: "npm:^5.0.0-next.65" + svelte: "npm:^5.0.0-next.268" svelte-preprocess: "npm:^5.1.1" sveltedoc-parser: "npm:^4.2.1" ts-dedent: "npm:^2.2.0" @@ -7041,8 +7041,8 @@ __metadata: "@sveltejs/vite-plugin-svelte": "npm:^3.0.2" "@testing-library/svelte": "patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch" expect-type: "npm:^0.15.0" - svelte: "npm:^5.0.0-next.65" - svelte-check: "npm:^3.6.4" + svelte: "npm:^5.0.0-next.268" + svelte-check: "npm:^4.0.5" sveltedoc-parser: "npm:^4.2.1" ts-dedent: "npm:^2.0.0" type-fest: "npm:~2.19" @@ -9579,7 +9579,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.10.0, acorn@npm:^8.11.2, acorn@npm:^8.11.3, acorn@npm:^8.12.1, acorn@npm:^8.4.1, acorn@npm:^8.6.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.0.0, acorn@npm:^8.10.0, acorn@npm:^8.11.2, acorn@npm:^8.12.1, acorn@npm:^8.4.1, acorn@npm:^8.6.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.12.1 resolution: "acorn@npm:8.12.1" bin: @@ -10310,15 +10310,6 @@ __metadata: languageName: node linkType: hard -"axobject-query@npm:^4.0.0": - version: 4.0.0 - resolution: "axobject-query@npm:4.0.0" - dependencies: - dequal: "npm:^2.0.3" - checksum: 10c0/4d756b5c2ff099f1c7f99e55a5de9b2066cb2a13a3170185ff34bfec2d7bcab81eb820a4e7340d35c251341b61ebee6e705b7ce64db78224df1df5a4d68448fe - languageName: node - linkType: hard - "axobject-query@npm:^4.1.0": version: 4.1.0 resolution: "axobject-query@npm:4.1.0" @@ -11556,7 +11547,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.0.0, chokidar@npm:^3.4.1, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.0.0, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -11575,6 +11566,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^4.0.1": + version: 4.0.1 + resolution: "chokidar@npm:4.0.1" + dependencies: + readdirp: "npm:^4.0.1" + checksum: 10c0/4bb7a3adc304059810bb6c420c43261a15bb44f610d77c35547addc84faa0374265c3adc67f25d06f363d9a4571962b02679268c40de07676d260de1986efea9 + languageName: node + linkType: hard + "chownr@npm:^1.1.1": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -14673,16 +14673,6 @@ __metadata: languageName: node linkType: hard -"esrap@npm:^1.2.1": - version: 1.2.1 - resolution: "esrap@npm:1.2.1" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.4.15" - "@types/estree": "npm:^1.0.1" - checksum: 10c0/28d6e36adcf4342a844a938a736132269c33e9db6bbefc98c6af5ed06c14899afcc85391e7ce4824ce5066877fa10b0ed5c5007592cbc58012be95f13c66467f - languageName: node - linkType: hard - "esrap@npm:^1.2.2": version: 1.2.2 resolution: "esrap@npm:1.2.2" @@ -15085,7 +15075,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.3.2, fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": +"fast-glob@npm:3.3.2, fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -15193,6 +15183,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.2.0": + version: 6.4.2 + resolution: "fdir@npm:6.4.2" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/34829886f34a3ca4170eca7c7180ec4de51a3abb4d380344063c0ae2e289b11d2ba8b724afee974598c83027fea363ff598caf2b51bc4e6b1e0d8b80cc530573 + languageName: node + linkType: hard + "fetch-retry@npm:^6.0.0": version: 6.0.0 resolution: "fetch-retry@npm:6.0.0" @@ -24069,6 +24071,13 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^4.0.1": + version: 4.0.2 + resolution: "readdirp@npm:4.0.2" + checksum: 10c0/a16ecd8ef3286dcd90648c3b103e3826db2b766cdb4a988752c43a83f683d01c7059158d623cbcd8bdfb39e65d302d285be2d208e7d9f34d022d912b929217dd + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -26577,23 +26586,21 @@ __metadata: languageName: node linkType: hard -"svelte-check@npm:^3.6.4": - version: 3.6.4 - resolution: "svelte-check@npm:3.6.4" +"svelte-check@npm:^4.0.5": + version: 4.0.5 + resolution: "svelte-check@npm:4.0.5" dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.17" - chokidar: "npm:^3.4.1" - fast-glob: "npm:^3.2.7" - import-fresh: "npm:^3.2.1" + "@jridgewell/trace-mapping": "npm:^0.3.25" + chokidar: "npm:^4.0.1" + fdir: "npm:^6.2.0" picocolors: "npm:^1.0.0" sade: "npm:^1.7.4" - svelte-preprocess: "npm:^5.1.0" - typescript: "npm:^5.0.3" peerDependencies: - svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ">=5.0.0" bin: svelte-check: bin/svelte-check - checksum: 10c0/acbcc04c8c6ab7baee7ccf36ca134dcabe49fae103aa92661e7f80e01216623363fb794fec9a3f794f7003d55629373567ff485925dc33272f48cea63e7b2452 + checksum: 10c0/192ee83f83169408b5f0b819440349f53e256db868d59fdd2422e831ef581891f5f257632dd3e632b12518ca307e1f99ff97455f56c19c3d2a5ee7be6391a181 languageName: node linkType: hard @@ -26635,7 +26642,7 @@ __metadata: languageName: node linkType: hard -"svelte-preprocess@npm:^5.1.0, svelte-preprocess@npm:^5.1.1": +"svelte-preprocess@npm:^5.1.1": version: 5.1.1 resolution: "svelte-preprocess@npm:5.1.1" dependencies: @@ -26723,27 +26730,6 @@ __metadata: languageName: node linkType: hard -"svelte@npm:^5.0.0-next.65": - version: 5.0.0-next.65 - resolution: "svelte@npm:5.0.0-next.65" - dependencies: - "@ampproject/remapping": "npm:^2.2.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.15" - "@types/estree": "npm:^1.0.5" - acorn: "npm:^8.11.3" - acorn-typescript: "npm:^1.4.13" - aria-query: "npm:^5.3.0" - axobject-query: "npm:^4.0.0" - esm-env: "npm:^1.0.0" - esrap: "npm:^1.2.1" - is-reference: "npm:^3.0.2" - locate-character: "npm:^3.0.0" - magic-string: "npm:^0.30.5" - zimmerframe: "npm:^1.1.2" - checksum: 10c0/6a686847f887d2871eabce4888916cba6aec5bae924a76fd01f4098db1c0053d4e5d6434070d0a048eac75eaddd4fd40e3fae625a0253464f7baa6b0f147f209 - languageName: node - linkType: hard - "sveltedoc-parser@npm:^4.2.1": version: 4.3.1 resolution: "sveltedoc-parser@npm:4.3.1" @@ -27521,7 +27507,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.3, typescript@npm:^5.3.2, typescript@npm:^5.4.3": +"typescript@npm:^5.3.2, typescript@npm:^5.4.3": version: 5.4.3 resolution: "typescript@npm:5.4.3" bin: @@ -27551,7 +27537,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.0.3#optional!builtin, typescript@patch:typescript@npm%3A^5.3.2#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.3.2#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin": version: 5.4.3 resolution: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin::version=5.4.3&hash=5adc0c" bin: From 47541e8508b74e1d00ee329e9f49f963e96229c1 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Thu, 17 Oct 2024 20:41:49 +0200 Subject: [PATCH 39/53] fix Svelte vitests that broke when upgrading to most recent Svelte 5 prerelease --- ...-library-svelte-npm-4.1.0-34b7037bc0.patch | 97 ------------------- code/renderers/svelte/package.json | 2 +- .../portable-stories.test.ts.snap | 74 ++++++++------ code/renderers/svelte/vitest.config.ts | 8 +- code/yarn.lock | 36 +++---- 5 files changed, 66 insertions(+), 151 deletions(-) delete mode 100644 code/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch diff --git a/code/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch b/code/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch deleted file mode 100644 index 212dfcc7d0ea..000000000000 --- a/code/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch +++ /dev/null @@ -1,97 +0,0 @@ -diff --git a/package.json b/package.json -index 195dac9ee7d42fdb76bb22dc37580fa0bffd4680..980ad42f41a06023f9f7e370fd382c9217c24be5 100644 ---- a/package.json -+++ b/package.json -@@ -55,7 +55,7 @@ - "contributors:generate": "all-contributors generate" - }, - "peerDependencies": { -- "svelte": "^3 || ^4" -+ "svelte": "^3 || ^4 || ^5" - }, - "dependencies": { - "@testing-library/dom": "^9.3.1" -diff --git a/src/pure.js b/src/pure.js -index 6d4943412448c9f310f007ca7dab9d04cef90d0d..d62f4aebeb1b23ccc3c3d82aadd67075c6507c0e 100644 ---- a/src/pure.js -+++ b/src/pure.js -@@ -3,7 +3,7 @@ import { - getQueriesForElement, - prettyDOM - } from '@testing-library/dom' --import { tick } from 'svelte' -+import { tick, mount, unmount } from 'svelte' - - const containerCache = new Set() - const componentCache = new Set() -@@ -54,40 +54,34 @@ const render = ( - return { props: options } - } - -- let component = new ComponentConstructor({ -+ let component = mount(ComponentConstructor, { - target, -- ...checkProps(options) -+ ...checkProps(options), -+ ondestroy: () => componentCache.delete(component) - }) - - containerCache.add({ container, target, component }) - componentCache.add(component) - -- component.$$.on_destroy.push(() => { -- componentCache.delete(component) -- }) -- - return { - container, - component, - debug: (el = container) => console.log(prettyDOM(el)), - rerender: (options) => { -- if (componentCache.has(component)) component.$destroy() -+ if (componentCache.has(component)) unmount(component) - - // eslint-disable-next-line no-new - component = new ComponentConstructor({ - target, -- ...checkProps(options) -+ ...checkProps(options), -+ ondestroy: () => componentCache.delete(component) - }) - - containerCache.add({ container, target, component }) - componentCache.add(component) -- -- component.$$.on_destroy.push(() => { -- componentCache.delete(component) -- }) - }, - unmount: () => { -- if (componentCache.has(component)) component.$destroy() -+ if (componentCache.has(component)) unmount(component) - }, - ...getQueriesForElement(container, queries) - } -@@ -96,7 +90,7 @@ const render = ( - const cleanupAtContainer = (cached) => { - const { target, component } = cached - -- if (componentCache.has(component)) component.$destroy() -+ if (componentCache.has(component)) unmount(component) - - if (target.parentNode === document.body) { - document.body.removeChild(target) -@@ -109,9 +103,10 @@ const cleanup = () => { - Array.from(containerCache.keys()).forEach(cleanupAtContainer) - } - --const act = async (fn) => { -- if (fn) { -- await fn() -+const act = (fn) => { -+ const value = fn && fn() -+ if (value !== undefined && typeof value.then === 'function') { -+ return value.then(() => tick()) - } - return tick() - } diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json index d2fd3a847f7a..b5180a46c612 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -66,7 +66,7 @@ }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@testing-library/svelte": "patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch", + "@testing-library/svelte": "^5.2.3", "expect-type": "^0.15.0", "svelte": "^5.0.0-next.268", "svelte-check": "^4.0.5", diff --git a/code/renderers/svelte/src/__test__/composeStories/__snapshots__/portable-stories.test.ts.snap b/code/renderers/svelte/src/__test__/composeStories/__snapshots__/portable-stories.test.ts.snap index 6042c29439c9..7141b7af1311 100644 --- a/code/renderers/svelte/src/__test__/composeStories/__snapshots__/portable-stories.test.ts.snap +++ b/code/renderers/svelte/src/__test__/composeStories/__snapshots__/portable-stories.test.ts.snap @@ -3,6 +3,8 @@ exports[`Renders CSF2Secondary story 1`] = `
+ + - - - + +
@@ -22,10 +23,15 @@ exports[`Renders CSF2Secondary story 1`] = ` exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = `
+ + + +
+ - +
- - - - - + + + +
@@ -50,6 +55,8 @@ exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = ` exports[`Renders CSF3Button story 1`] = `
+ + - - - + +
@@ -69,6 +75,8 @@ exports[`Renders CSF3Button story 1`] = ` exports[`Renders CSF3ButtonWithRender story 1`] = `
+ +

- - - + +
@@ -97,14 +104,15 @@ exports[`Renders CSF3ButtonWithRender story 1`] = ` exports[`Renders CSF3InputFieldFilled story 1`] = `
+ + - - - + +
@@ -113,6 +121,8 @@ exports[`Renders CSF3InputFieldFilled story 1`] = ` exports[`Renders CSF3Primary story 1`] = `
+ + - - - + +
@@ -132,6 +141,8 @@ exports[`Renders CSF3Primary story 1`] = ` exports[`Renders LoaderStory story 1`] = `
+ +
- - - + +
@@ -156,10 +166,15 @@ exports[`Renders LoaderStory story 1`] = ` exports[`Renders NewStory story 1`] = `
+ + + +
+ - +
- - - - - + + + +
diff --git a/code/renderers/svelte/vitest.config.ts b/code/renderers/svelte/vitest.config.ts index 58fc647e8f30..8d2664dc9021 100644 --- a/code/renderers/svelte/vitest.config.ts +++ b/code/renderers/svelte/vitest.config.ts @@ -5,9 +5,11 @@ import { vitestCommonConfig } from '../../vitest.workspace'; export default defineConfig( mergeConfig(vitestCommonConfig, { plugins: [ - import('@sveltejs/vite-plugin-svelte').then(({ svelte, vitePreprocess }) => - svelte({ preprocess: vitePreprocess() }) - ), + import('@sveltejs/vite-plugin-svelte').then(({ svelte }) => svelte()), + import('@testing-library/svelte/vite').then(({ svelteTesting }) => svelteTesting()), ], + test: { + environment: 'happy-dom', + }, }) ); diff --git a/code/yarn.lock b/code/yarn.lock index a81df9a4ec6e..61e3e672ce05 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -7039,7 +7039,7 @@ __metadata: "@storybook/preview-api": "workspace:^" "@storybook/theming": "workspace:^" "@sveltejs/vite-plugin-svelte": "npm:^3.0.2" - "@testing-library/svelte": "patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch" + "@testing-library/svelte": "npm:^5.2.3" expect-type: "npm:^0.15.0" svelte: "npm:^5.0.0-next.268" svelte-check: "npm:^4.0.5" @@ -7320,7 +7320,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:10.4.0, @testing-library/dom@npm:^10.4.0": +"@testing-library/dom@npm:10.4.0, @testing-library/dom@npm:^10.0.0, @testing-library/dom@npm:^10.4.0": version: 10.4.0 resolution: "@testing-library/dom@npm:10.4.0" dependencies: @@ -7336,7 +7336,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^9.0.0, @testing-library/dom@npm:^9.3.1, @testing-library/dom@npm:^9.3.3": +"@testing-library/dom@npm:^9.0.0, @testing-library/dom@npm:^9.3.3": version: 9.3.4 resolution: "@testing-library/dom@npm:9.3.4" dependencies: @@ -7401,25 +7401,21 @@ __metadata: languageName: node linkType: hard -"@testing-library/svelte@npm:4.1.0": - version: 4.1.0 - resolution: "@testing-library/svelte@npm:4.1.0" +"@testing-library/svelte@npm:^5.2.3": + version: 5.2.3 + resolution: "@testing-library/svelte@npm:5.2.3" dependencies: - "@testing-library/dom": "npm:^9.3.1" + "@testing-library/dom": "npm:^10.0.0" peerDependencies: - svelte: ^3 || ^4 - checksum: 10c0/4335d8be01bd1e6475062be218577fa1d1b24e9dc97c33db523c5af6b044b97625f65a58bb5f73225064c5a2bf9ae9696948a2bcd2d82c1c25423014d635dce2 - languageName: node - linkType: hard - -"@testing-library/svelte@patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch": - version: 4.1.0 - resolution: "@testing-library/svelte@patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch::version=4.1.0&hash=490043" - dependencies: - "@testing-library/dom": "npm:^9.3.1" - peerDependencies: - svelte: ^3 || ^4 - checksum: 10c0/95586fa05bb536fb538d01731a705121d71797a77ab7a8e1f255909e50dfe4fa09f3a6678a60b8a075332dd45940c0fa37d002d2f6c201400295fa3840c88821 + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + vite: "*" + vitest: "*" + peerDependenciesMeta: + vite: + optional: true + vitest: + optional: true + checksum: 10c0/a83d662ee7a0ce901598bd985b8d6afde72c7aa37f22447078bd38c7ec9df6b3fb15464d3f171726479a65f0e562732526686b6a33d6b2c7fd34edb6e7b706a9 languageName: node linkType: hard From d22b8e92510f04bd9895d9b0d18b5d1a2ab1118b Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Thu, 17 Oct 2024 20:42:45 +0200 Subject: [PATCH 40/53] fix types in vitest config --- code/renderers/svelte/vitest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/code/renderers/svelte/vitest.config.ts b/code/renderers/svelte/vitest.config.ts index 8d2664dc9021..f80ba91df63f 100644 --- a/code/renderers/svelte/vitest.config.ts +++ b/code/renderers/svelte/vitest.config.ts @@ -6,6 +6,7 @@ export default defineConfig( mergeConfig(vitestCommonConfig, { plugins: [ import('@sveltejs/vite-plugin-svelte').then(({ svelte }) => svelte()), + // @ts-expect-error -- types don't match our TS module resolution setting import('@testing-library/svelte/vite').then(({ svelteTesting }) => svelteTesting()), ], test: { From ac09301a97648ef72a21fbfeb6b5e2b2a8da81ef Mon Sep 17 00:00:00 2001 From: storybook-bot <32066757+storybook-bot@users.noreply.github.com> Date: Fri, 18 Oct 2024 13:17:58 +0000 Subject: [PATCH 41/53] Update CHANGELOG.md for v8.3.6 [skip ci] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e13bedee71..32d7a9325c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 8.3.6 + +- CLI: Install Svelte CSF v5 in Svelte5 projects - [#29323](https://github.com/storybookjs/storybook/pull/29323), thanks @shilman! +- Svelte: Add v5 stories to CLI templates - [#29382](https://github.com/storybookjs/storybook/pull/29382), thanks @JReinhold! + ## 8.3.5 - CLI: Update the React Native init to include v8 dependencies - [#29273](https://github.com/storybookjs/storybook/pull/29273), thanks @dannyhw! From bb07fe6b58cbd84f3b69733b271f9cd25e91a8b0 Mon Sep 17 00:00:00 2001 From: Michael Cebrian Date: Tue, 24 Sep 2024 19:23:37 -0400 Subject: [PATCH 42/53] UI: Fix refIndicator to use ReactNode for check icon --- code/core/src/manager/components/sidebar/RefIndicator.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/core/src/manager/components/sidebar/RefIndicator.tsx b/code/core/src/manager/components/sidebar/RefIndicator.tsx index 7c49c95105cd..e34f28d744d6 100644 --- a/code/core/src/manager/components/sidebar/RefIndicator.tsx +++ b/code/core/src/manager/components/sidebar/RefIndicator.tsx @@ -7,6 +7,7 @@ import { styled, useTheme } from '@storybook/core/theming'; import { global } from '@storybook/global'; import { AlertIcon, + CheckIcon, ChevronDownIcon, DocumentIcon, GlobeIcon, @@ -216,7 +217,7 @@ export const RefIndicator = React.memo( ({ - icon: href === ref.url ? 'check' : undefined, + icon: href === ref.url ? : undefined, id, title: id, href, From ec6c5e0bc1f6be645fd7a1ef36af560be6e33dea Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sat, 19 Oct 2024 12:04:09 +0800 Subject: [PATCH 43/53] Review feedback --- .../components/sidebar/TagsFilter.stories.tsx | 8 ++ .../manager/components/sidebar/TagsFilter.tsx | 35 ++++++-- .../sidebar/TagsFilterPanel.stories.tsx | 19 ++++- .../components/sidebar/TagsFilterPanel.tsx | 76 +++++++++--------- .../writing-stories/custom-tag-filter.png | Bin 0 -> 130402 bytes docs/writing-stories/tags.mdx | 6 +- 6 files changed, 92 insertions(+), 52 deletions(-) create mode 100644 docs/_assets/writing-stories/custom-tag-filter.png diff --git a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx index b897d80a4cf7..4050986a91f1 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx @@ -18,6 +18,7 @@ const meta = { }), applyQueryParams: fn().mockName('api::applyQueryParams'), } as any, + isDevelopment: true, }, } satisfies Meta; @@ -65,3 +66,10 @@ export const OpenEmpty: Story = { }, play: Open.play, }; + +export const EmptyProduction: Story = { + args: { + ...OpenEmpty.args, + isDevelopment: false, + }, +}; diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index 5e2823bc9597..66a955a29361 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -11,6 +11,16 @@ import { TagsFilterPanel } from './TagsFilterPanel'; const TAGS_FILTER = 'tags-filter'; +const BUILT_IN_TAGS_HIDE = new Set([ + 'dev', + 'docs-only', + 'test-only', + 'autodocs', + 'test', + 'attached-mdx', + 'unattached-mdx', +]); + const Wrapper = styled.div({ position: 'relative', }); @@ -37,9 +47,15 @@ export interface TagsFilterProps { api: API; indexJson: StoryIndex; initialSelectedTags?: Tag[]; + isDevelopment: boolean; } -export const TagsFilter = ({ api, indexJson, initialSelectedTags = [] }: TagsFilterProps) => { +export const TagsFilter = ({ + api, + indexJson, + initialSelectedTags = [], + isDevelopment, +}: TagsFilterProps) => { const [selectedTags, setSelectedTags] = useState(initialSelectedTags); const [expanded, setExpanded] = useState(false); const tagsActive = selectedTags.length > 0; @@ -52,14 +68,14 @@ export const TagsFilter = ({ api, indexJson, initialSelectedTags = [] }: TagsFil return selectedTags.some((tag) => item.tags?.includes(tag)); }); - - const { url } = api.getUrlState(); - const includeTags = selectedTags.join(','); - api.applyQueryParams({ includeTags }, { replace: true }); }, [api, selectedTags]); const allTags = Object.values(indexJson.entries).reduce((acc, entry) => { - entry.tags?.forEach((tag: Tag) => acc.add(tag)); + entry.tags?.forEach((tag: Tag) => { + if (!BUILT_IN_TAGS_HIDE.has(tag)) { + acc.add(tag); + } + }); return acc; }, new Set()); @@ -82,6 +98,11 @@ export const TagsFilter = ({ api, indexJson, initialSelectedTags = [] }: TagsFil [expanded, setExpanded] ); + // Hide the entire UI if there are no tags and it's a built Storybook + if (allTags.size === 0 && !isDevelopment) { + return null; + } + return ( ( diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx index 108b39440973..999c5f3fdb04 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx @@ -10,6 +10,7 @@ const meta = { api: { getDocsUrl: () => 'https://storybook.js.org/docs/', } as any, + isDevelopment: true, }, tags: ['hoho'], } satisfies Meta; @@ -25,6 +26,20 @@ export const Empty: Story = { }, }; +export const BuiltInTagsOnly: Story = { + args: { + allTags: ['play-fn'], + selectedTags: [], + }, +}; + +export const BuiltInTagsOnlyProduction: Story = { + args: { + ...BuiltInTagsOnly.args, + isDevelopment: false, + }, +}; + export const Default: Story = { args: { allTags: ['tag1', 'tag2', 'tag3'], @@ -34,12 +49,12 @@ export const Default: Story = { export const BuiltInTags: Story = { args: { - allTags: [...Default.args.allTags, 'dev', 'autodocs', 'play-fn'], + allTags: [...Default.args.allTags, 'play-fn'], selectedTags: ['tag1', 'tag3'], }, }; -export const BuiltInTagsSelected: Story = { +export const ExtraBuiltInTagsSelected: Story = { args: { ...BuiltInTags.args, selectedTags: ['tag1', 'tag3', 'autodocs', 'play-fn'], diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx index d9b50eb67833..ed7c9496d73e 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx @@ -7,16 +7,9 @@ import type { Tag } from '@storybook/types'; import type { API } from '@storybook/core/manager-api'; -const BUILT_IN_TAGS = new Set([ - 'dev', - 'docs-only', - 'test-only', - 'autodocs', - 'test', - 'attached-mdx', - 'unattached-mdx', - 'play-fn', -]); +import { isDOMComponent } from 'react-dom/test-utils'; + +const BUILT_IN_TAGS_SHOW = new Set(['play-fn']); const Wrapper = styled.div({ minWidth: 180, @@ -28,6 +21,7 @@ interface TagsFilterPanelProps { allTags: Tag[]; selectedTags: Tag[]; toggleTag: (tag: Tag) => void; + isDevelopment: boolean; } export const TagsFilterPanel = ({ @@ -35,38 +29,40 @@ export const TagsFilterPanel = ({ allTags, selectedTags, toggleTag, + isDevelopment, }: TagsFilterPanelProps) => { const theme = useTheme(); - const userTags = allTags.filter((tag) => tag === 'play-fn' || !BUILT_IN_TAGS.has(tag)).toSorted(); - const docsUrl = api.getDocsUrl({ subpath: 'writing-stories/tags' }); - const items = - userTags.length === 0 - ? [ - { - id: 'no-tags', - title: 'There are no tags. Use tags to organize and filter your Storybook.', - isIndented: false, - style: { - borderBottom: `4px solid ${theme.appBorderColor}`, - }, - }, - { - id: 'tags-docs', - title: 'Learn how to add tags', - icon: , - href: docsUrl, - }, - ] - : userTags.map((tag) => { - const checked = selectedTags.includes(tag); - const id = `tag-${tag}`; - return { - id, - title: tag, - right: , - onClick: () => toggleTag(tag), - }; - }); + const userTags = allTags.filter((tag) => !BUILT_IN_TAGS_SHOW.has(tag)); + const docsUrl = api.getDocsUrl({ subpath: 'writing-stories/tags#filtering-by-custom-tags' }); + const items = allTags.map((tag) => { + const checked = selectedTags.includes(tag); + const id = `tag-${tag}`; + return { + id, + title: tag, + right: , + onClick: () => toggleTag(tag), + }; + }) as any[]; + + if (allTags.length === 0) { + items.push({ + id: 'no-tags', + title: 'There are no tags. Use tags to organize and filter your Storybook.', + isIndented: false, + }); + } + if (userTags.length === 0 && isDevelopment) { + items.push({ + id: 'tags-docs', + title: 'Learn how to add tags', + icon: , + href: docsUrl, + style: { + borderTop: `4px solid ${theme.appBorderColor}`, + }, + }); + } return ( diff --git a/docs/_assets/writing-stories/custom-tag-filter.png b/docs/_assets/writing-stories/custom-tag-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..3579f994da12c14f9b97ecf32f3e34be9f455d85 GIT binary patch literal 130402 zcmeFYXFQzU)-X=AkSGZu1W|$%y+$X9h!!n+?{zSvjxHh5JA*Mul<2+JBzo^wIp^HxKJW8CdB6U@JYVKFd#`=1y{^^wTKoF^PDP%Wh=vFY3yWCcjjTEr7JeHR z7LNLD{F@m#*PU1_tXoi9O&ySqvXY27z>(9`0$^sz>FMZv1INM=lk{{pHMh3}F_>9e z+d7Fe?KQSCG1yv&GieJbb1OT`SlZaW@piS;@K(_@_qI0|wqTN!AQJNwxk2D)2{L8y zbaZe6ig=1M{R6Ma&HvxiTucoASOT&aXVOuA#~=f6wPXxi<_5=mzU#a1qaZ}31sTY;RIy)$Yf>ofPbSBQMUvF z9Nd1BuH|HVLp1N-Af3Nq;!HgJ{9HVL!*u_@u;1)p5c|iTT0l!z8Gz#rL7;!k{*z78 z4*wecbLe3En>Qj3rcT!4Or9JTmR6>24j?884Rcq^n>VomfW)|dbJM~0UpW3T`U{EJ zKNu(SKX&@(6aOKPn@9gScO&N;h2i?Q#@r14t#g)6H;U$Zqfwa6+&V0*yOOptGVc^* zWEkGL0<3HuEU~cOd`{FR(9!6j%+Oa?u))Ut`e6b0-ZSj4ABJze;R=e3!M)AWDfO=W zOE^)9p=?`7N!U|&sqIftxpH(zP*4RY<#ulx#ju<4*@DmF+}WO_^%Zb!4g%i8%5({0 zi(So3#aazR<&-mG_w)2TQ&Oj5z)^0);$LcCSyz}I8M%j*6nL_Sm?sYCh8IS;7Ww&m_F;pKm@*FuM2lMcvV!iFn?>DoZl`#~gmWF~ z2L6}tEsf*Sgt(0z%yU0I`t}`j$hRVsc-F3WDkgW~%sFkDl>+6?WbS`N&cjU=$j$vm z*rQ1CsmdpfO`n)mf}hX3mNgYrs(R^#sMbDodjUTgqKK9?%@2P1)G;KUdp10g6fvS^ zGfL^W6aKz4m<#7#0M0FJ@@^AloOmfpvg%|0qc%}XRqiG1XWdw@mh#Q;Uq5-~{d1p& zfT$ftMku|oREH&3N=6}dg=8pf5(xsy0Rqir1wDyeIBydO?;GVlWT8Jwx}72Q`iOK- zOy`VyfmR1`d7bgtq?OO1TQ>jUOn`iDX8;S&r-yIuvfz{NILpjYp*xwECLX`1O8*EZ z>0fCS|Me^jy8p=;IxJl$uBQ3q@>%alCe9W?p=<1kIj6II{$>cllEgc@=PTZIjLts| zvmOiX5{NfSCw&Ri&v>w=I_sa-PO2(nDesb-nZQWlAGR~ZJh3w|kFT{_f5ju&t22g= z8lc`jn~_`(-M(Y;=&F*Xh72AJ?NBjWFK5=lF)`~Ze_ixI^m$isA@Z}C8?k4&*3Woi zH`(ibp?wPkq#sz~w(EYCz1>HplGyqM+d9r7RWl(LGGy_CD=4Pt0SX$zW0-DefF0v+ zoKRXS#1poMpFMRBO+yo4NpxW_7*hZB?@>5qQ#el7GK<3cloz0UZ0B7vV^)rLu5}l$ z0sw@>bz^q}2+Oc)oFzEh=sa=!$#B0*QG6okF}W2XL0$rUmU7D~pg!|qLXf42Kt9%^ zHmfqa?OPZroR_y$g7}u0>h6{VH##4Kf~Yd7ns8wEa2Xjv9|9jTdT^tSR&%%rhSs{)6F9q(ugf@!`? zZa61=tQc|Tr>Bdy%8Va*1(ImM<&ZBNf!c_B-*5h8BriHlW%C%HTR@4F?*}apWoLVK z83j9Cyjf-0{i3^tom*wBr|G3;bULIb_t=*2rhgXw{(v@&ZOO-nSK^Lwg!U5rz`F^r zHB9m+_yB+_(*9{lYM;rO(EK*_hi4zmUQwIA_{H#xe@+ql!9$iwE!sxrC;-d@Ns&ue zF;Fs)pHmi-c3_iWJ&Aqu1SbxsCzL{NIrdPY<7@R-=+~96FTSdb5>(L4By7JTVajh_98fYct&(aea0sjZzgw;Evtr`V4IMWz>&b(%kq{D!KW2&Cv%+DsZp5^{Dv48 z*|Oad-+CkeJ?HLHAlp0Hx2Ix1#Oc|d|AWb$_L(_(|-4n5JTL3=mb_^RUS;3~XXRJD*jY}Bv6 z?IGn|lHj3~uIcYD2R6nTx289o-6}wD-P|{;HjlRKkPTa`TPhR9xAJ0ON#Hx=@?oLo|i6=-_S!}dqc|^Qp27l4{viUkKQZMp6LODVvf~C_wQk5E= zvH>|wvw#%w3R{gj_HrK!Jb;w&iaVXBf9td01EwJQDXmH&vAX$?eusG7v>9vQBNQ%* z2{pfl=E$dH4+n*7Q4UZxC*>x6;jq@3)LSUe)+sOIEuPeYR(k^+&7l^}Z4K=WeXkco z>@{pO?14i`PDpFfnVQ*?RJxHU%Vfve!QBdRyFDwIt+B0t54M}L2bpjY(B|>$hik%0 zVnyE5%JQkq@bb3u$%Cx;42=&Ou^Pc+eZP`^XoPS*_H#cKvKCsWUFR6|khh7x{YZ^J z+xDkD6_~0;F{iIO_9(WU**e;R_N;fJU2CIZ;W+8h563F_ty-(v(p~ypb+icDiBvIK zGx~XS8av&4etRBdI$Jwm8?a{m-cYpBuF;thrs?X?{b=xH;n%!CuT_Hcc#>MQZ@;gn zM4FF?52WQ=%OhXWi~b8wzX`wS3yurCEB#AY`OdWNHgr1~dk}jxAQ*d3*eD&6P9CUZ z^2S8LL_fy;-K%$U!@(w~rHG~3r3JFum`z5uSA`^uEmS2_f(dxzzo}VcjDZMFq<;-T* zKQkiDe=0}7@1Z#*82oH6=5gV;y03uQub1n7rv5}L1S-6U6Y0GBm=D&$I)LE*Q+@C8PECkbUgYS!`|9!P9$)=_$sH7U z`P$A^6;v5XgtmrUzgptbVcDQOzpZ^6mV^0P`ev_pzefi$0^Lm3O`2g|{`6e7wBl8z zphfdgRPRF4JS;w;QZzX|EfxHxh+~klR*4tF@21}PQ(B#UGi6)A>v+}3whm-ZA15SH z%41A$)m_``VAxF!2k#UECR>juN*5|^Z9T`Jr{V_FqwS~G#*PV)L z%-jA>hi#f5!>mVGZCSf4x^yEbBGII?pk@Cizm+$E7PJrq@v{omJSGwypIe;=?U#-H za+L3n(xjgdZ#a5*Jbht)*>xux4UI|P5QexMx}bipHRg4uH%#6i8+8}CII8Dsw^o5# z!o>Xs*XLqUM(d=8CWbb38-C?26UE0%drqy79e{ws)>W5p2Nmmx+!M8(u98zjVK0IG zEr|V`Jyof^5ui2Jd1Z3IMM%_%(8+Cnu>NvOr5!rtu|CH<_oBYkt93hYBzQ}5`)Ydr z*MiHD>C%r$d6(#2i8V>TgC~1E$%ALAo2q>o5Q)M~*G-X8x;fvov*~px*Ap%YG{yC| zW9tb;crwA&;dNoPhZ11@#b)8vRQTe&#@|6eB;`RSAoe z5XXoi?rbWaX*ZTCFMe;GM-J`mCGUR~>nO#-f03F&(uQ=z8ji;{X+u%F6&+sWJ^ggp zUm&$4X^Nm$Q=7xi55C^x5#QfiW5oj3w5?E$Pdy}%;-?GnR1Jut71kjbvs=#MpAmwPQ_a8oB-MY;X@V`I*%Y*;bga0*y|8)ib z>k0qY6aN412?JnK-CXfaBphw_*)o!s24a7p=v~#VU)glRI71Cs?NLSXHTI!mu$|ta zo_kVQ*tlf>{n6~{6a%Dm10@%6-xIA6E_*$V zSPxYDvwp-7W8bovdDovwdW(yCDQARYkAK)E=#Lg9_4zj0!@e{=kyoKKv6IS>L{v)Do+>)Z8fW%Y*;N(yyFSQtWbk zw}jijPiW-RRTC!M(N%Q&Xwiy`;_A4{L!#SSu+7{Zaxu%Qsk7LTL* zm8kT|^-m83D&gMV{hAx99ZrQEaa&g2V`F0qHd{+-$NZ;&MA0B6Pm1eygiMa2J>7UZ zwV+l~IovwKEAhuIzTe+?DxDqW3RNpDiF!!;tiztO!#>PA%k@uNFa$`^%&D+?CW-Xm&^x#pd#}EFSHvV(& zscWDa3F`yYbbO1$Tj4f!hpG1tE6V4~*|+{IH@bJ3)3+2m1Q(>ZzNr&8Q1rPy=zEFN zoTB$Uaakb8^>SS94+cQ?Mm3WHho{3;xr2AGt+Yt}&U=dUV~X?BIj(dwnnLj0_8*xJ zVh=PUTA(=Bx#Q~-XgrfcvHEn}-qaw_I7(CYPrFGmAa8x78GknJvPu4~+x*Vat!Lv) zN}27on$LgQccW95prIXv(dZasE(RdV?D^#~UHWobJ zN7go@-X1s1!V^({;=$Sdr?UYH_cI*~v~P-PdXhaX7Mccc%n3n^9S@zKCh!o@$q#!U zWN>?c(SA+mXMhw>mg=iY!Mbv_ zA7cdLa7y^I;gJP%V%IAMw{7y3668WHV!<&+WI^zAROP&JHc&0(&yEUAXd!A7UnB{r zN571fiD{7DdLkm9Yq{flCAGqp#P9|j#n)XQJU+tqQ=e-qC0IV7eKVtTYBrXy6?3SI74*!G@S9}sFC?y*9Fj49lQ?Rtg4gE z3orzyt2}}W!r*4q))LAuss3Oh?|N_HJ?6G;SG7+6JX_Z{hw&EKD_=yxibec^w2;5Th7=DGyK(aZ^-2COoG_Qu2QeW?-|B}|oyhf4c(WCY4nacI*!jguq;zv$(StALJ@uDUafJMW_-A3fBSgYf( zx1ZzKfPFL&pQcchn>=5d{zY}ybWjGLnz#pKz83zzWgP|+uvyMp;rv!q)Y5H1;=o}- z@NWtj-UeXlP!JIOMeDzW+`(8@ot5>g#SJ-mY1tja2=obYz>#+^)SAOAuzv2Ha|@mX z*7kr}#Rnmvev!g_G7Y7+r(MD0C_LdCZq<|4Z%V-<>x((FOiI!&Y9uJc$?;{Cv z1D28}OvC@IO18I;@{5WtmY3Rt7Yj;Dui-MJ;^Tb=Ns){j4`xpT=V!&{&sjXgTV2zr zsxpdUk;NI6OX`dy&lk;!yjx_CtNQhKCEQP_IP@eM+~4g$t-OAeF7XyL<|L;xy!_cMf z5q@};^{cwNTD8=$@r0b?hg0O^6mxV7B{*6?_2nauJE`}N$*ZC@w{m!EG|!a_ofz@j z4hiG8>X#?b=k>_y-67+q)1@n4`#qtop$uDbMgCWRbhB82W>g!Kt^U_wN)A2#PB+Hl za@_d!d2}a(1D}U@TCz==-KDHA%ZtAC`ukBV^hV!b%NeCSZ!z{A0KzxRi>a#p$L2lI zG-A&C(A*~d*hU_oKSk0T#p+hdT>o^BdAIJc_l3QZP>Tzmc1wnW`MsJ9diZLxb>BtJ zw@EbeOa37dA_&D31seS2Xdb=u9_G+`IoaeW>VB;S0BLKM65;<_O(Ga1SdZ`~aKHg?@0Qi&d_(C0XWd#rmQ>lzg6kUPw#2>v@Lm#7aig>Q6HVc#cB z)JRm5BZ1Qr+wVh>wTr3fU!kb3)TJpo;U><8*Z+lo-LX>U8u9#s{BJdYmHcV)FVoM! zUTgH+YF~(v;IuV=K+F5EK|TTBt?H;A5PT8{&o~*=lP5Lk5w5jGl+cxy6jC?3;P&?Q z|1gSsTnGDf}ZPoDq~1G zE!IYv7leD{GgENw?Ce@DFHl3*D=Fqtx?{XfmxtBHoedKfs5#88Se`n~wZ)fci@bp@ z;rmgHjf?v8`U_6`Z zRVsDg(11)U=s4@fOebk;*6PuY?BDgZk+{ky2GU3>!{+`L9BaS&6NTO;wrhIc9mzLf z{Vt~oetyT|UQQ~@&k^ZZi48=8x^dw0)l5uPdb&SL!nu{VKIx$P$zC#|?QsJ>_9G$?Xv zS|@nEM&Q%@9d))EEq#^T9k}=5VxQafm9=9}btd5pL_YTvRffW~3Rxd;e)np-1zd=k z?`R0p%d??O;wW$dy=J+p0N>|1Ax9wGzcho=^v+ zl#*u6^mCl|a$j`1G>XZhZ}z5D$0E6l{a*-R$P8F8S4b9r!WhGW?JK^Cd_M8j5k#r` zjioskB=&Y75Gmcp@lIb~w1d{ZslAhSyCrh54&|tkW*zN@OkU?yt%51QWiM;&A$a|0 z2G`FJg~eIH#FLs5F!D%EOG{jEF+ZL3zwPTA*hxEwdT}{2TtR^gI+OG@%t;-!jo%WU zE%bY4tPh5`E-X{#my|3F83dAR+stg?)ti%e(>|Gz2pT^wG#}d3PO>%P#6*eKoDc7Lj4#Cp>|ieJ?GO)MT-%(iaaqTkkDjs~KKZjBH^AI55RMx;BP$`Vn8pf#7H8$*6itc4L|D2L=t7 z-f$L?$qHG7oeeSp0;NBDg%JB6JMOTRfsVaL2nexpXH^;OjYF;KMeg64p&Yg((@@^5 zQaGmF(P~X7EpAp42DscwNKAAS(#|ZWd1UO|#dwnvWz|IO+8@4%PLRDiPHODCzmo^& zRD@o26D^am0x=;4hl-@GN^ zc`iv2w?!L~#`))rUMf=-_lA+Z25;wdWE3Gr#gWn*Ev$PkPo}OHHMh>uG0k&Lkt{Im zG6Mrm5np`AFUNCk^O)T{J=6qGfuEO`+{ecInE%1bzlKs|*r*WC7`t(4tiVySe2`&C zB;RyOaACgN)FtuvNdSjZ*@{zgrkh%}}rpUYl!Rf(lF;ZX>QgqLC&q%%$?_FNi%KV2R#%EDrd_w;`;0Fel+N+*$i4a0pZnk47; z=vZu~0I5mbIq4Ev7XtAt<9fPJ9F_WMd-JR>(34XupxM*o_^Boz_OWEg6wvvd+0MyI zeT+q2^gP#VjwxQuuz8+=zbO$4_*VSQUyS-&>7~?eukvl|7g909pjW@t2INeCFrZ$L z@dAXbFvrstMs|fY;p{W_<8s!@Ju6cjR~tgm6x1_{7M??85!}(z^Rm6nTq74oFRf)V zSBN&W(_d4h_Wz)ZE>puF7!x@O4WWngRwAoL%mFP)XE~Y2bB<%SD^7d!L+<_yS1gaQ z%TnUkgk|mpKWX*e6`S{XY!BNtpZB=b8J0mKRSV<)K6k%WSjrXMORh&zo*9j%_@d*& zha?0Fwtum~;@yx!z1_cLEU!l}-2qp=UwRdHOs00lD{MAhb?d#0i}V@?XF>+@e&SD_ zBuiIcS_}tV@p{8zN5AFfj&shVy|*6GSacK!R5&2)be-rMCWLB3?iL6{WAl!Sh1{iO zfQq=?jSdNUeYU@Hc6gP3R$+^xoN01Pm8ii{qR^tYAkCc9ajb_Eq8BD6T51 zpOLR(hCf1UrZSInZFX%>hY3Z+>2fGg=W_Hbe)W?Ac}_*e^p)7>dj7=-D^$r?|I}h! zo{^{A*4CE!mcx9Fp+tIPTTf|zevGX=9jm;GuxxLi;!4eTnx~T%IN!Gp4!5VEQ(e3B z<)zWYkA%V{jC@gso1;1L>CQ*@csG|^)H@piQRdnH8D6%zVyNXJS*=mdu_;6qb6)Oz zi#QGbv9qzhh~w_qZ>!5=?&=L?NKTyB7&bZpFR60PEMDZy zY7d|HY}wWqo1<48cdA49u{s+@l7cz@Ism^*y>&u-*_q4gb+k!kv1SioYZ_K;^(-Ou zf!9gx-t@EDMf4{K zVJ*|LX;8*FYq{P=5tp)vjMGOicB7hCl#rDfQOGS76HIq1v zHw_x`{Yiyw{HaAeGx<=3b4Arvj&KrP3-*OGlPJ`==h8?X;<$D(nal4N|K@0Y{8*I# z1zv`qX|BICrEB$`*_{Bqe+s#xEiIWdH7#wRm?o}SJL&vgQmKxkAAZZzWWUY1@kdl9 z;SP9og4_2ut%B}MP`9AoPd%-e$7MNp=aga(q0nJyM&IVU_VxuPc0V?L7T=H?bCgfg zx84L%@qzE&)EXpZftPi?%ivltipolme)LA(!27aC42jW zyks(92L=7!D|EWePR-<4&({!GwT_F7{(RWt^CvrFiYjME{4>_8`zdUetxlM3Kg{mR zDwj@^CcWo2wYAlpB%ra&3#VE$h*l(gGvs=XL%-I^Lrmqe|6s7f&nRYhzM*&87wvSQ zVC@d7SBOjVK3M#VmI9>bp=1bMy|JGntQl`Nb}sB%rkaL(Og~cpSm;duN0RUjjr|4 zpDy-LmTMWxjP(n+PrOmo0iK#HsigoOR&Qg`155w1_p4~W-kkDh>|LpuQ*s1CitBG< zXZmC+{PMb6e3|+l%^HyiDbh)tyG#>$HrKK~fzgo@Ce2^CL&U6?gE{d@cz-ADeP6yZw>ih{7CNcy zlNyyCr*IZ5D_D1?s%*^pu81F|1mksv-(D42YiMuuvS=)HzC$`xMf~4q^gy$F!#P)1 zZr~@U6JO+o*a8H)=N6wEVfFhOPH1TjI)veLZod$kcWGtksmyY!M@pX^*5{>3>6Krk zF1X8yR4zpk(7Od0H&m_2tzB8u>?^!!tqcVj(@Xt*$L7z2O;`+7uRWi*FB}y4tNi#syH9ugbptbd2;MaPC=K zbMxyIYO_Vb(rEuhKNz3AQtcvtrBd&!i{iu$`^@GA9VPzU6L!TeW~ktfSETfiVZqdc zqx0Z#jW0^UCWZt3T*Q7HaGJP6*o*kW=ws5L%VAvdWfo#TTQ~y#inmB!r)tRH>x+!1 zC;sWwajz@4_dk+tw8>eJ=!&)jTiY|7?Tjob4? z$0iuz!C~_hT~$UcT^Avq-pZZaq)Vo1uajx_%rn6u&SaZD5R%hluMt^?4j%9M+S2H_ zZ~{C_t`4e}Cvsh|aJ!#q4{6DqbK7nisO{QuxlAYjcc#dodMjES73Zblmskmc3$>o& zjX=b{W&C;ShXv1uq;D;W;NKjAmJWuBXFNr z=QwlFFv#nuhVk&e<69vh>Tv#5D2T0jry(wv|2ximFH z^*d?0PDMYr5q|T&WFlz%04X}wlnxa-B!52k3sKW-gppls0YYU|{fvQ<*D#6?_<@MR^Nh@7f~qznxW%|*cjxdkUa3^LMV!J{*^ zn|Y*TLp{Yh{X!wH_fBaOEBGX(aU@x46#VmYITeZMgug+PLBc4za3tk(Z&d#)KyLyr#i^i3KK4*BoI-C7Q|N zO|!VoohNq`9k*c7v3_eA#^&cCD}(fk0L}7ZR^oeSKlh!-Wvs92;%g0}+3td-Oj=`b zjdUqpZ|O8xtaVtOB#Zi%saSv9JPdVg=kTbmy4+8BDAFU#`uIlm4v=NsS+>qvMV zP4n!0m|AhhXWB2kC#akqtym(eojyaRqBM@0zovJTR{T#&7}k|EyGr}lW{NSX4jJ=ykX_NP23Ft7t8vD^3keD7Z4Jbapopu2fue*jx%4zZ zU+>mDP~3^Y%7hS9sA#HWk)itXJa*xF0U~M-?hY4W`PO-1#Zw?>p2$!-qa=Loe=AE%|ZmU~mC*EQe{yeHI5+Xnh4HKoJonw7W9=yXyp7YECrdQtlN4 z)MUHS{#At?_|3V;=l*TfbI$quUOSQ72o77(8cW66D#VO9^wMJrvz#t7B;d!e=c|`y zHoLZEEnRZX!3t-On79fkGdA!p!nh+PJ{o7-lttKYs-J9dxU9Jf#-N@kAV-`*Tc&v_ z^VyquSvPf#%44^iw%rOxW$C-LcFKQ;i<|z^0_N%c{Sl#+V?X7Yw_1sbgK6iLi=Ofl z{MgZEgaI7-whPsKwyWHgQR*Q*Ct2wCFD9$(2K1~qm_vyNPw&63Ix6>>NR7Ze|435Q z#RO~>Q|Au+B}34Y7bQN*u5liQnnJQgUw*jznD=RJ`QUQXPDW_DPcy(UdC+QyU0s1t z%-o7f^5y_mRbo4<7TWXs05(xGO_M_oy6qY%fyZJ2%pdjzha9IMIdq* zA*S>+5648V0eV2BQW=9Ul{#HBV7&Qp$f`MCu+4e?NtYmy%BJwx~@BHqxO| zTl>SR3~=Zh>E!3}6p!bj#}sF)5~jkLg6H$qwf&{|(LS^gCXdsyGFr1;?c=3%-m|ug z=?Om){$o|{o!7@aLOndnu$0I5iAVbv^A*vbl>}E_x)s%TC3?`Y@nvXs{-W_~-x(B4 zN|oSgnwbLcM0LjyxJTARuD(v5Y1rlUC8^%SNIWMOr^4p0RXNy(gOe@6o$qhtNk0=I zU$6*GZ+wYv>#ypE6>ZYq+m;DHS=u|)D>Lmwnwk7hY>fqt^ z*e|T{x0ul){*d^JY`cz`G~U`cHSvhJekIBQQzk9q7`6g&dSqDuJLT#yF-d#9ZB*iS zA_l)7Av`pf+(UO!!MvgQEZcIbN!=>x75gC8bU(s!@NP-`iDS|NtCRpx_sK@y)al_U zFZr-t9AoSd&r80NE3`0q8*e)o^f)K5f%@R2aH0DArU_|`K_-vQZz8XTHUQzMF>35fT>~HXL}AtfP*-WO1I`?hs6_vg~L~3 zlhkDppW6&00?MLx1+)PAoD@nfea2`BR5s8S265MX{~r8QE|1+f-Y<4bruEZh&91K5 zt_HjzmFlK{F1gXtSZd*@{w=Mjv?!6v4vT#Q@3~z?LcU59Z>!J17?n=bsuQ`LAh1{$ z%}@W}?61P6l*^s%Z{a>ZHTKE4k73HF`;8!;y@!x0-@JOYH}}4(2neX_>M{}&M^^lZ zm>FCe&1$rE-z`CALgG6$dzI8~AaF<#3EW<5PlL=8G$MK%Ab zX-uCniDj6k{+3Z3eQbQ3|)?rYwTwRG~z*psBLM3JHz%qb@I;Z`>hpp?DM{&lmB|z@_D1t;_Q`rB^ zZ1u-Inu}4E`>mo{@Q;YEBTUJX^bkny$}r14f~pJ8!OoZI*c9E8G*85F# z_hkfJk(Dp*-`r5(vx#`+-!|P)BW9H5c{a`JN9Jn#(xSN0kkP7XrB73t zi9*K(pXZQ$Ye*$7wD#I6<4ELdw4>-kOoz)fiVa=^71tf8aYY|jA(A?cMCfPgTrbIz zFu^(vrRbOAU*VcJ_d6IvA}VbaNTvKF^baw0w3on4jg$ zc3q$aD!^Ft>${23RP0Gpi(aZEvyyz#Q;W6-55)_7xt<4Ca&e<8(|9n->*PO;TQ|}p z-uHTSautvb z8Wk<`ZgAlBQs4j&pkSg#`9qR;WbDZ1#E|3JWHtvUSj;_1O-AEq{s{X_ZMzc5)KaP_ z-G=VR2B4)*!9ZjB3GHrMS`Binen-5aAxT$#=3yvU=`;EghWhEK&Q;8tAi{@)!R!y4 zho*Er5*0LFKJiE&d6HPZJxKUS2@GX?Z@S>#it49ipLz!hfevzy0b}3lFY$FfW*LL| zwb#FyXG0jYeSVQrxiNXE`~#eB!?4h>dvn#KHd@wFPe`}EwpA&r(wp;N@-Qu<60=$OPcqmEaIS7y$6j?QeE8Wo1fkEDpk*E zI9-O|KW?qq5Cr0U#~S5t`jq5Z_^l*qB}z<*-uI@Jj*iV+ zz4^S5%NxKAcnczBH)P#gkk0_6(u%vqfjCDo{+oCpp}uZ9VFa1iE@H-Y?}!Z{YFy8N zK&nN9>HWSRygd2zOkFE+9ulI`<#nD{P@x{hJG5!Ows0?65)-#AZl*&+@8-JrMO7H~ z@p9Zn{;ivqR>o`N^t{iy3&R3G6qALe6IK7FIMEAV@e+f}!f;G*nY>?D0b%8X~Qu2-%t;WF(jF~&L50O`j&~^U8815djEvEdOWCBfr6~-ucQs(P zn>WsBe0OcCa(>M{HEkdWN8HR`3s_(W$=!9KWcDmYpp3mssx&ghf+9+_hv*L{?hGCl zr{?$9Vnc3Qux1|MVJ1vhh);CDlPMq2%-FkvdR@7isA|I|!JhLf1YZ3yzUwS7hA16L zu}AxD6*CdpWA9)GnSvr;Wc&Z+5wQZB?nKa4*8&GUB-5GQ+>Uic{e69dn^5hvd95zg zoY3TIX}s-?-zBt-n7|6JzW4Pv$E%zYiMsH~3Bj~lhj?imOqxkg7WR-{-2^|wOVGa5 zj9JNM4VeDEuTwBc>CZE|7b|7!Z+hl_y0B!4W^gYPFmmb&_1^ld#bPQ4-RBDChj`$-3F?&Rg@qBVxP%?;PWn`Q8C zJR^bd`DgK=7)Po%D?(vl8T@K?j5fI-CRU7NOoWb6`KHUO!huv5uV;8NH9*o+_~KPII$5u5 z1h+$iq$g*r4{_jC@9SUut~0iH{UQ_FlzNp-;yF7f5n-2SuLCAurV@E-Ch35xSh{TW5gy6dAX~lds$`z7= ziG@?WM5w23q`nevpR+{iJvEcq^`8kX9t3RMt-YyvD-_qS8(z0G4_r1ESdOi6RDe33 zdtsWR(-{g{B*qJ8*m+8g*>VogyAe}W&Fk7XssD0DC;m;rT=ue|@pzX2fuw?x*Gk~s zW{7EMdr#hf_9KFqdQ@h$F!c2T^XQBrjP`*${b%ncCwEAZfZ~Lqsnv%L{Q^!k%qy-( zPP==@mFB6d*UdVtqFXYDL4=b{Wl!A4`3Ba_Rp63>lKR3Qd7~T*qt67?2gMlJ{WBiK z!53e|%Mb`DP)!xI-&Ac8_fYO@>2BvIIsRBL)LD6RjX`H6Au$)|`EEGOn$Cu@G-BJz ze#2|WdN%f|or_Mk!P2Wa;I67w^7X(8d)bZne<3LKftWeSNK7N(b1g6LGx8#nGdsD&pF*jwZfepqPT+Vtci1#;Ef37Z|t z3a@?Y8SIzxH7s%q`kZs`NhJ6eHRbP;@e{TEKKD}TveI;qT=J^PDf%15u3*b-FTLqb z@uAI$6fC`(&<|H&fMSRxpS4W)=E|12BH!(rXvp>z5a<}4GGl}i3bDv3uTXCm{fJ13 zH5K-X82AiGv{lH8U>-x3?T`vpA~WlO^Q<{6Ig@L0^E=eROaLT-OAMfagT>YE+Eg>p%j(rJ%&L!Wt zi^CwJ*G)AChw;acstkv{dj^x$ys~iHcpE+?brPjUu1-Z8XP^z#+-M%9^|LW%S2P_9 z|9xrp=0lbED9K)2`$^Hh%FE+iIAXa{H8!xi-8yI!fs9B>+w+mCahT`)5lRv_q$FC< zt<|5#YIWB7g%tKggGBgEQn3z4pY~=%>BL|W7qsI`r3Q$)C$Rdmf4kln$`EP<%F$)r zdtue7`34*goeq2rQA7FeG{>Nt925bAB8jh;!cKw|3}uN2FiN6WP5dsKc?8gAjyU7w>;vc`57s7wOAMzq{qt_ePQ%q(zoy0z|%gGj$7 zV?V3C4fTD7%=He08K-;PnCp-YN?-Bk)6-LTT2Z~bCw1!)GTOrpJhtoa!;@*z3c zz4yKiCYC$q?$9OKg1aQA{8gfj-Dj@`2(9CCc%!CcAH0uhp*tIRw&v%m`iZa5$+I-Z zXHrZxm47J*yQubk=xTK$BU|?d{UwsM^cig&p_P}6hqF>}f{0=%xVl@2Rfy_%n+tth z5iNy%2bYlD2I_#{J4fs1+t?RwC8>{H#-REp_vvN^vyo`)1%sDfm;MmApEO7b<~aNB zuUzC9mgc55`iJ^%F3BvhZ@SO7UcI>Kn^@3?-sE+MGZd<=r(qC6R4PA&RMmDLsc8K2 z|M2zIVNq`D+lVM4A_@o!5=yCbOM{3=OE-hkEzJ;vh=_C#CEeW}gFy~mLk~&}FboI- zGt%GdIcK}iZ}0Eyf4GM0f_dNdto8K$+{>hHSW{8RN>rTe)jG3{t5sft*$tISkhK84 z*jZ;5m0G(dZUk`z)Dp#`vmOwAdaI-XphRc2%tBJ+Yk_+C`-2h7v+u);M~Mb!>f_|m zVPr9YZ`4a`pnX2xQ8&DpK8r1J4cHz^e0gWaHM)e`<7f6CTmbW#TxWN#THf7NNN&rE zPLcJRnW!>&Rb%V~R<@X-jtysjm)`lk+E<>?Og+uFa)VRc%cp_#<_a$>Jq)Z;0Z)hwIfEhHr@=V}X(# zD}a&Gx-Rv)EP___$*=CxJfc_nHV()?KZcK3l1satj1CPHe{L$l(aS$1_8*JwVm6Fq z(yt3ajwZr>27x?725a#wikFzug^Tx*86L`qv$Nxq{!V1tCSIEH?gG+hrIc;hg4)*< ztYmLbkA7OsEM9mE3cah8&6zHr!*8Ot{l+kMhkleLz%O4V17ytRTuyO#GHoR~G?0h3 zX1h8p09P9_rj9GN>U*kMne0C}UgKhQd79ZxZg1&hj6*qT3Zj~%h<=e})>>E-s3bhz zNt8yRa_2^mv7L*Q18F->xyOKspL6-B-MJI;b`5;dbTO*UqfWvmqmc(+4PxBhV>+H$ zXYK`mp9=MxU~zK!MD;*h?BRmv?$E6~>B1;(%#%OI8rE+U-nzBw+Fw}MCo;(Y$~W@% z=6tIyj&4T`KiRytBf=`5d^kI|mkT;m+>hQ%8))x*{tEg@0MEt`G^UH6wxh<&y`-nJ z4Ssxp968=7YXC8#4qWCW$>sIdvlM#h!TKxx2aa(LQYv0O^rHFe+T|e~p?7z(X;wRc zaF>cyCB`1>wnrPuAf&nfG9Ev@=Cxlw6sJgaa&agX=ia$0i^75W{_|!yA)UnF)iH(sk1arftN|vZ{MrsQ zqkMSq_-ws+hOVWs&{ko!!g;v3#?uBC;tNB~Q3Cahp50B`4$R!t%d{Gq7@jC54^2^* zmec$>?!;X_kc9~9JJ9PNPw8V;#62h%198#dx3&#NLS zLIGZrir$Ujt$F5`3&`d&{D_bob$9#wVG1G&jW zO>n<|0!=Eliuk(-`{KRAMDaQpm;sSiR z%2=ngTTareZ6c&zd$NhqjtR#TchTvK=mHIjjv}(&;-P964+R#nlA* zJ6t#VBgbS?%DR2gOoOKL0^yZ}HkpP+ZyxXi?i+!3uUeqdylJu_7D;LA3}$_^nx@8t zv{W|31j&>9nU*5>1WJP%Ru<(!^Ia>xx#t@{GQQ6TIsOUEUD!#TRja8|?O`S@%qJzf zYS>9hFy0xx2sX1OOC!^TVg$vF|EHr{!oa0+1G%}!vld4nQ&^gB(DF=F(V++Rc zSgDG_#XcRkSS|Yy=JJ&zJZT3JBEs|B$T>aP+ypMH=+NuC*v24IIsd@p9Q-U*NJRqTD_| z!5V`A;rzWX++*^x=cP%@rG1!|TaW~f0@1r^XoIcprg*%?ZBy@7bmp z@xZXX811izqIrS5WFHdi%P^xhQnWMEE2onDUgmIv;kA~}Y(<3#D5otM8R7ehEFx~n znFGzJ-|fLSG95$%3OjoqB{`4D8RT~b`Fe3Te?V$qpJ{uMu^Kd&+Mz2N$+>>O?Hykz ziO(eWZpxEyQs$ZDh=umQjrwFXJXeT+{Q9(@;F4y5h%`#IaA?Ln$sS$!<^DR=H%+l1EMZ^x2`wdn4kDh{6`84&X}I+J zF#blj_vXC8le_Lz4{2r))u97YL?UUPqgr$vo=8=uGoQT>s^6v+{^OE+rlQkQWQ& zG zzE|?@W$}e+!-{{_lcXkf-ykhie0aHYZ1Ui8V;sHjFr^MiTGnJldak7ODNfM4@*cWJ z{7ksLe)zRq`~*xCc92Z}Z~b%SdSNT|;~Nw8Xifb-0WncB+GU>#FLHUE*Jo@^7lud0PMTh*vQW?3WG|tt9N>|D zo&idKT1nvSpJ_hm3cF0{k#t`3LSEh{R{(#h`1Ta&(X;C@FA!=JCB=fucc&DwYnZVg zyH{-&>m<)h22f? zNBk_=qV+=<_4w`;ZUt4MFvHGcR3k&rarO!qmf={IsCXGsVT08SfBV(6;O#9XKqWyL zUTuqF#SE>-_KrsDnaS7lZxEE;m3-RbkJo;Po}BbA?>N4dds!LJF`Tdv4dWJf20xP) z+cx6SOHox-1yJ9#2K9FJW%|{cx)sI`=Ng>jFVtVO=w156Z!^MlN5VU1e{+&UPwqh< z1muqqnrZj-ny8PYt&hIOAo%!EWx{nRw-I+Zjl%Ga;*yfM;t+aWrKG-kCV-hk+;htv z((R5}JFo2%$0{d@O{9QmPd2BTja#M?6ij^UQ@)kSR@3{YS{zqAD|AxL9Myo|v&&Qk zpEXkpJx6?MAxm4Cp|S3rKO{A2z8}KS_5?>ORVJjKB>Ar|D%m!^r<0x&a8O#rUpqH| zZ#Y_r)U>WJLSNsmR^M{{P%c>$^oPPEG{_jF94^I!ode&=2w&RwNVI#jig+c)P<*7p z7U4Ln2W4~CAwap`G%V#N(X|h%m)s-ArQB9KQ%i zTC%L-S{D&){y>no0Dn9N^z5JQkXgm!W+v|Lc)c*yO0&;3ReZ}6&%hrfX>`+1@7lUvc zzGb1a?>~RD(n1(@EwuULu3rR?m(*V2DYiZ)YD*ICw%A4-x--LEwb z&>9lQUb`cj@r5GuRUUb{^!ca+K;m+?23oEiU9O#UC<_ZU$qsvcY21PpDJN7a3 zDU%Xe4VmuSRP8FefPEJIqD*yCvy2dnBs+LDH|_V^Aj>FF6!GF!oOxlSv&crVv3j@i z=9?oh=3fVFO!(k{tXP;M>}bQAsw)%iqEKh!F|}>*uhFl&8<{gdO$Y?$5*r|Wh6_Pw0P`LPxd&TA4bo647Y25 ziMJXvPu38yu>R$L`NS;$OS)yotgvG&Xyrq()=EOW6j!>q7j-*vviJtYhtx+G{r1MD zsvRAq7o{8_#}3Ri)5Qgj?>!x19ei>Ic2JoKZPVBj%EE>yN6PU#cm8lxGQ>BEcm=jN zgno>?NDU0R{)38oo^=zg2%Hl(M=c!#1J_FVUjwL`2f0#&tV~N_Yv#?cI!;p=&XQN1 zLE1;k`A(hVs0zb-mpdsWt5cqPAFCcbUT;o^!2A32n+>c3Z`u5s@^_s#-6eMJX!Be= zd@IOp%$j(7J$A^NVZ*3#k7-=t`b9Iu=|N@3Cm#R!8uiQ$euwoN8>6{}WkxWysWMaM zG-2nkX091N1x9+xld8g91EkF&6!eamkr2!6@msbSSeBIjj`#Hg=1@e^$ zEj1`}A%o%Vi||hmet}WMy)X%nurHyhcF^6y3w$R38C8}crU8aq`F9?@PfszU|M(U@ z{32_t?Ysa_sV<_EIBh?=%ZqplxPT_Y#bo*Uuna;+)b$VfK3f~y7`Ywr`1S)MGEzPQ z=w|(4`-77`O$@?BS|eb)X6p3qbw~;asSbPQ1>MEUkU1th*cs+h&GDxcHPbK0%6UgW zPV@aIv;aN*UN3tqQc+z)q%ry6T3kK%eIju3)sy93pKH>|;m>x$73JNLf z#Lc__9OUsC$j9N8113(`VU|S%fc6Wb_xg`6P`T|0EU2aOF3w-%+#RU+*51?mJ(5vL z%e*!r?f4C=67Zi5e?mX$4P=tafDWcGS~WN?J9hPk_@4c}1$zV{X6R&$ygU)St^QFd z)3PAbj#6g)EDUU`)*}}b2%w8iDS^@}#npA1P=Dgj?K_SMMFU16z15%q2w3*$QL-4| z3*nawq++*MB%}1GD@~unQqYB?q{2Fvyf-<%s}DOFx3O)G(Msumry~%Mo@AM*kAEk! z^Pwa&GKf9HkH5RgK8brTN%~yl^7XsxzG0Blqdq;@o~7u{JWsE*^GOn?VcqlYh;NZn zAkKfy?cJx=caUPad5ZuoJw);bGrhpl-pYEN$d+DJs320j9^uaf3@^QRK#tXSV#M zC+i{Ub7-&6uL7hd#tV8|Enl*C+F<96ycV8!IKGTBT(rYZjiOjxuOh&Hi%D)7(pvIC zY1;|qsQP&TGRvqr&U#kkky%<(!_UrC>MtxTq!@qLX&OJX_NdtLtH7(3yL8@-DFrr$ku6oxKmj}4WU#L}rJrkri;a8Xofh((k<-U87W>R{*ZTP;mt6Pl z3B?xt2Wem4-oyRqu*fuFrCKokhR_C&N(u{o+Tu?6tl!&TzIKSckbff@u=6i|5<0{1 zfhu>A)Sa6^AV1UW_kP0BfJGc9sED9F-{COSMcjavWcY?QxD*zwot(RaU@zkb(qX;x zUrLQTAiLl-Ux&!`VP5Ak7zPPBm&4X(m2L^RdfV?pF0#Qt0tvt@2LAM-u}=C)z`=M; z_j{{ijIc@_*`K{Kru$1o{QM_rRLVp(_p&b3J6xKq;bS=Zl?h*c+C?j2J;r~0IoKDe z#kDqKgz&vP&LsQHw_cT(<^2A+m+T4J$L-*LZF+Qf;?BdOip_@v5*)J zD2VizTTcA!g*%VpEST4pEK0C-?sG;bMT*P%5j}qKB6@ z?p;t1T%N}CLE3<;zPi*wf&_NnZEsY8Q55d#zxS1LwNU!3+SrcJH%-Jj%FgTaE_-hm zjmhmql$4F2ve?N|h_oGjF4ysQd78eTyTmhJlFK2VQQtxsV|z0Fo-Cesa#m07k>q{* z^1iQbIVyg+P&NK0^X%e>$c@g#>UrM7ES|VUC<&sBoAIqI^1FURh6zvToZ=gZS!`DjOL-H^W-?UOv#wR%BSu~ss>p-A@NUQN^!t>$^>qRG9kn}$wkcMTEXoPg-rj?QZgS+W3IZ-W1QvOKVir0M(ww>_7omlJPG=Owx0lG9)aweyFa)r^KT zBI}L^SP0hWpZeZK2eT_LiY+cb#Vi1`_o1_O3C$qu3ni##rK0}x_?_*Sf{-@jR9$BZ zGS9>OFpZ#mL}mwD>vsCR)=WRS0SO0^ohTyk$)>gVFwg61t3leszTpaJX~((bh7}=w zF&kL|r~Ym6+5udyY)Y3hbXLxK?ggU(f4|lCe7*A7P78V^JA~1bP)iu;R38l-Jn?90 z6OO&ZVgDf_MQ#r(wHdJ%lSZfXHrx?3%iKt0EVlsCRfS7#>bm1~6B4P&--^HxCXZ`9>@24{-#jwgjpoR*`5ZsQ5X*aa`Z(MVMo;OGIgZ+~}w`V%=F19S1 z!*IWdy*NI!ZIyq4&cQWS{PMC;E(q*`vr~v=oF*Z^$Tt<}XEN0q>{=nEE~!Tal$+)* zd;8f>onoo)1eDC4ZY&O|F5j|4?tY)^skPFbC@#!=N%&Gaej{3HlhYq%#7CUc;(*kH z;e^0H?wv0s;S-LmHZPJ(t<;SgnDmV`I2x_CCiPr~)hz(ftdDDj8GGR3eNLBVODr{b zH$UmS=lPNxc~?TZ=WOv%q^|i@L;meHEH`o~skLJW`g2a&DJI|q`^?J|-C>H=gPjRA zpUa*v>DhNd=?^rGgmmqlQ$?IRjQ6{EddMJPG@5o>1nC;?civz6^rB-io4>ifHDn_~ z$XRR^n;B2a)6|(Z$VYf(MPsSY8kEqboTQMhanYz=t1`}%*pw;Vt|x8AbQOS&0r;A> zPdullQ^5FPy+R3w(O?$WhcZpjv43T41=}4uR{K%f(xTMivfIq>bj}xRX)Y-pB~;}x z`_madnmZH|>yHy@T<7#+XsHAnu8~1DP#puRs&mUVqU^s1rT&xA(d%@chIqG~J{8^; z4&AgggjQlL1czQ_Tv#LCNRMhB0DS9wec(=pl<%%sCE`7aCYHD>1n4EaO;SX3#2Jkc1KU>>VGL zKuZYT?m+a$^4pGCq)33{?)r_$THX2F8>i1lCeS-#)*B5up%e&mh;`;MXRB7 zTR~&Kp&Mlv)4RT@+LcWO2nh6S4ZKTV6NPPs?R0UT-?pU_#&>OO6E$yXCTvSt4`px# zw4XV<&Ul^oq^G-|cL$srWq9uuC;KRap2nDvRP`+=JPB_E;Jmvt}eb2KV;n^m5i;PKkr@M8{`3v9QmuOiy=5q zOE+SI$m4)Z8O+qmhUS}QPf%y8Pm_fAxt-Sqafb@6SAy$3j)~6v-an=O+dj*8o}&EP z>^;v%w-@nKcqlkJeiE@~;n?O+HQ)j;kWul?Jxr9+Dz@*r)2LdTVn6{lnJx*gC%cq< z!AhYGosq=aW*+S>dj9w(<+O6bBL(tUQ`l+T)=$)O4JsH z{I>DEy>c*g4f0CT#F<*bC-St>Ia6vch@&#$%Ru|Ueuw>BgJV4U{LD*0S=l z746t4f2q?7hD%)+@{M0)#Q5qNb2BSMJYUbB60jbM`$C?%>FzS@kMDaQN!PZ}XDxMV zd)STOjs*yw#6Hy#GkD85!Irsa>O_6I7yQ}I=iP>5XV2>S{C`?X;HHhc;Tw|K z^+!*`r8i$H7FyFSiHBi72;Fvy(doYV(=HhMw@S<1^y?Bwc(ik^QKQ(n;*R7RL|gkQ zXZt69$2vss%{`R#*(eEQ!vzd(_S=llOfVWqCcb{V$UtL5gHl@&dX$_sE`5d*8SjCu zjyXfVko>S=a`;*GvRqZ@g;(dc@e%ix?IlIt}vs6Jn#7E{W+A+(NyRHPrG`GYP2+iZ@CZIxfdo=J_wX**gnwy z6$o@!R+`Vj`@0Rt(T9hOUR%|B@rAAeFJ zg1^G?kr8zU)0@te1-KXR&3Vt$Ong1lUO)qUopT$IptqCw2Gs4}xP(8tA7j+7VKeKC zS7H~7BUrXz$ypT7ZHU*}&1{$Vb$9Qgyo5)&xY{=3b{y zEw_CD(vF7m<4Dy}=cr>L2Sl47q1ll4Ap3VO>7!JKqakGeb{D>#FKS(xpVov&Y*8i7 z)-sfPcrTV)-n${C*&PczV~tK-B@H+X&xCIjX4pa7Xq|?r&KD$1s5O#!23Q>Dn=~3y zSI^rv%xXV^+5|U7W>9 zjq8f)GZp|A$Y<_s#=yg3^v}D(le$Z{R}hn!SUC52#yWUG@uEBIe9$@9>RU5>Mu8yk z^Uaats~r+(p_H?UT7c{&kv2_^ZRr+?&tw|Gi+GZ^w!CF8#w5w5+1%E+64cVg`-<5E z9<9mU3b{*VaP*@bFO@J@VbHL>OAa83efHPaI62TmKtYjCxEFNp0Z``j_`4$a8w~0f zeeV87QvEJ~ni9=B7qsHFB*8fii~IohEHEnYbM>&^v9S$c42zRx;TUF*@#U1o`S<8j z_tTW`Y{XkZFEtVC7qw`XT%T1gAxDDjqC}sUut&%CUwrnwNUQ!`<4zm~A$@-i;+FId zP5V8g@>Qm+CSQ%P6S(54dy0(7*>)J5Ctlp?ymH zaYt__ou1FMl0Oy+!+u*HR`fz)oAJPvKbKW_3gmd{uT9^OV@i1Je&rrZUaZJL=GBKc zeAXwa#+BD^hzWN$;otiW+*2~#n82|toU$=yPi}2(4S0FP)ka%&_uD%^ zt7v9zlu0$IXfdZ@$)ZIGAPieN5SD_dkmy0QB< z(H%aA=f-Y6sl$7eQKJ3*phhX5r>~7)^8Znw`)z8OK!n9#AAPjkn^Ze)i>%MDKJ}C| zkfgy$_#1d|ZG39}>@h`IkHH=f@RZ9b(#0auA4U+Me;$)|A$iWMjGBa*(kl4utsQ5% z>p|QJUd$t?Mpp{P%*>GyyLs-SP(l z_Cd}07R(pWgxi5%_Nd;*JE2_RbY8O?uE{YP+5%F)jr=55DuIEQ-$jvX zw1PG05!6#mBYa|eUT88W-MUgoOLy&TG*>H@RVkDcKCsTwctnxdh$e6OSjy)MGeD~p zl7_1S+@8W05V@AqBiYrh-jM`^!S~IcdC5q+k3bMc)k?}i!dv&K> z;iO2{gk!gDw-C;(F3FB8Gi25>^^q*G_xHia?0{l^iW`#h0LMVgYI-Sf%I)5{^@a%g zYBRWw?6++H?Z$w(M6knbSdsFUeQ(|8&2z-&PCmg69(PQ^hW$M9n&>^rl^t(3skRn<}q(XL@7t|#rSOYIn>sh+K+B? z2HlIE+l>tu1I_xGZMT$C_eFzmnpqVoe`Y)M^?J>&MHm+}^9|B0M>+#*Yx zd7dFa0VZrFuGQ=7LDTzD8(yk|L(>j3<`WFM8Nimbn0@R%;FI?O_ zOHL;|L3?TKn#JaNLn9KRpeC?G*0`dJEYj$6K7%GRzC77ym*yL1s({*#cSMwU4$ju^ zCC!*xXah7Ph$&~ zo3j=ktLDfzi@VI@*wO1lp?|b!|HGvGhM15wN-TXYYOA+k+bimJZ6os~W8sfIU$@m7 zcp4l=+i=|c-{@t{X%<@r6c8Wl=(&T8a-xT@rP@;jq-FX^Rf)HHFwB;|*Qo>|6#DTD zocVG-tekX)I~|K!pD$|8SHE1oJX(x8+fRb^&LE}q7zEIMj>rbdjnQE>u$t7-%5oDw z@fPDb>0uPT_}4@uyJuvBA4NI1K>|^aPd+rA(10L)sZP9}Jvm+BtE+mZp@YI6cNvnB z9J^l&*x4feaZx2~p0v{RT(8%feja?zxgG}I9W2q=TQ`sHS_yNQYf;_qz%MqP+_R#s z47^DEbZ_CPgw2jJ4ax=Lw!4$&;70bCzUus}x^?wJZA04JgDN~S0A%GSDNu3pErgyr zi6$*u_OtZqH}Wy0O6i@nwSjbYk<%jt#nt>he@b7$%9}DwNrcP;)<~WJ*Y=M--=}lP zATta-^!~}(4aI_=4z6OELtPDOt=a(s!qUB=bE%r}ypt2$F=JnNWxH)TSphnGW4#)O zneBH`=n4qQH-32O&oQO{XjrFmU|#hV9`(CgUfFfb1qPuYVZ=?dP~W@lJ>X9t=SK5A z+wIC!EMP!L0Bp*(SWRccXTWig4j9}kza9m0e%h)jR#oW!v#Xh+qBxPlO@P~A%;R(0 z^LTb?dV@K;uKrtl(E$sO&O4T`Z_MNt0-Ps$NBbMhlD?ivznCw_Sd}dR%d}ST&2ID| zLAs&>uZj1kOGBLYzJT~6;p66&ho8t-)^nrdwA925T9QF)Ufc8X`)uEf3o|&$CHJ0B zcvMzY#3~|T+HA=h`njHr4_mq?2MlU0_r}!~2Zz>gYKn=;qJP}qcu-Z$#|-_55U)1-vMiNq$yQubR!+lL;Xx*z_?U0*6_Sbj^&6Sr&SKF8}O;MWsH&yRs` z>}@4jr|THZiE%Awn|FU*ZMCd(Mw&XEp+PHg$~=i&X^l^BfzMCLS3p)&)Af3wTZzM@ z04+0ZAW*pPdnce4e-R&je$6?RdgYAMMBEFSY3y&C_x&&&+WRFwGcj8FTr)~+Kc@FV z)$>3ix_bgg==fr*{gOjOe=sF`Z`PSVWdh6Z>Du3>b&n;B?|riPv<8>01B}`e>f)rJ zGeKc07($S2+G$Z-ed^RB7VY9RXPFKja`c7Wxe(`l)cHJH!YLuSUY1r_XeWRx(VFod zYhpPCwWGw(`sZ&;h1n#e#_3xw_t$C_e5J3@TG|lP?3;=UlUy(1;7-bx3;AjV-)1w* zk2YX4TUBYX3R8P%qt4-t09?PRFUGdu)fRJ-e?Z*4=P~`z_ZmyIZgY?2Q*G^g*zbGF zFrIcFHU>o+BdK(R*sK!TwBAQ&{$&d_LKO9pJ_+glY?_Etc|}OzR*qC$ezx3YcprN@ zmCn;Sx5NB+m8x}I(Fy&WSMMpg#EnE0?)#V`vUULDACfX^)RR&?bavdG<_SG08^3ji zZU|6V?dljQ{sO_((v7DQ)>M+zoE?&%v+IF;%O|>3l8SS0dc2#yv#b^^c?;0ZPUZ`z zoZ=1xpIo~ivZoYla7cNtsGfI!^aUS|=-dt7f(NIj&}9clZa+Zf5su>m4uVx^gAY`D458 z9(M5;h?t}7JE4=GEK@&Q^vLg!!dYodSSVqqnDwpqdOqVn-ZPr+v~(w8-v~z?d>J|) z@>)qPt++u}l$Tn!C8THa5S8xhR1%VBGGsl(_wwXUMbAbNd*jB5F{wCz51=$kr(3i2 zr6;2*ch(y^epMW-BNLPm=uM8w7}6nW<|NTD+qM6 z?fr22qn`C?A@md+$fKOfZ{dbpx06~K{Iqo|4d_JkN?Km7xCEY4vW}Sv|23?z5hWZF zj~K!jld_bG&vaN|diR?wJk~5gn_e4ku3`wKu}_MHW$3=K0r!mIW=FkcsPzuVPgERL zfovK0z^&M4Eui)Lz(d@lI;U`l!@*n%V|z&Owuac*&+5Iccy+_TDu6Dp(2tr-_pa@; zrwd-g?fe_QNi331E4?>e;m9+)XS4@u`f34+J1QtO3B7H>+|Ng;4{y^cxBDK%weGZO z4W&P>uFf&okzO&wZ=6zGBA^rhCQHn~Btn-~Vba3FS>oii2uWMFDSoOr9FUZvq`^h2 zZ1~KP@)9W=4qvqvS$uGgZ$&EQ8_<+h3;OJ5?2QEBl^@v$u$I_wO)p!Fu_D9g%C)l2 zwvu4(CgYqHyuZrB4KgUT^t>D$jynNI5BIQ)ChS4PWY1fpUP*N_%m8DH~2)Y3p zh8{Dfb8_fb2;2MA$6vQ9nR3=GPdsKw;{>vpy8d|xQcgDsV;|F&N+2DEMDn?x^#HXO-YC&7d?Ha%(B42?v6O;Bc25svF~bylE&HNjNG7m9C{UX45fpqAbK(P1XH%xIpkznTdl*3R*qE} zw&^)TDlT{+nC|<|8g4mRGPy=`P@3P+pys|mq6#|+uJa^mSoZ<_YAef!Q};c=>bKcc z|5Q5V_g-SUBltS&+FijBTJWkczY?8@+Zrbro$lo>5a)y6d}y5xwp#HJqZ1Qama`h< zvYvHpTv!P_pGOb4EHPW~vP3j0I{Dz4y)O1>Xk|lk=hOJP5i>1_e%Ea}6P6h;QSgR?kpoJacM3AR4R>MX&CQoC(g!IoW5BS8b zAsEzCI;RI_usOKs+rm$yB=>kXoTeN(3+ntP#Y&sMT+YLvnyC2amH$zz_)oydrPq0) z?~%9BUQwDyp5DrOyS5yBRE3`#_JnNJKD0|j_^IODYB1COPL<<*%G$bE%!)%&ItusE zuDQrO{vCf#e{=iugX!*m*4Z6srcy?2gqzXvlAoh9+HI>L9p%?>5rnJVt_?ChazLSL zD}`@llrzg*71c2bD10||H+vO5Yuf)Fe=7jy^xfY6*>tjo#Y=&FO`CB~l7aJX3Ek&S z-0cw5eFELyq2Li=6LzbQBp)5^n_fCJZk#XdD~o>nxr|AY_*Wr@D&8|(^%$1MyS<2)U9Q&!bcUyE?Dg)6Oweg$3{cIa^s_ zLNSWnUG|KK((hX&){3^aN3tVLVZDF-{@;)54VE$IyWDu{1g>^;0YVmb4w}}=byKvv zKbNE^PlKDd4h_P5Af`(@dKlEqpWYlhF@I^%2hC;B?&F~?!ZDa?xvVBG`6!y^FDLKM zUoSDoD8_sON4T|`iA^Su*$!Q(@-sDd+xE+l>S}y(CpSPd;nC(Ra%r5m@v*wp28%7C z!^HSkU+s5&^#6WR<$ghQA(&01@9161GZ|M8K8nd@=nfP`Wa9vQ(=xbxSKjojE88@*~jCs;+ual=f8jL zv=4m8_aT|mzImF!KqWQ;B!(Vpiu1d>?m@uG=Wgx zzL9LR>L2$}lMJ*9Ng9s9xJ{3LH|gA84jiqF5j?!BF_Em#SqB^p=wZ1Jon*#6?d zr|MZTl9VO=P?ES{&s0>Z7{`l(3$=0Y{XGd(?NomZY|)Svr1(!Y+|nLrGV(VYM6|~z zq(eSDPYZijXifarrskjf&!y7I)szKZjMRPTw^VnWsYp=vz1A#cNs{GGtD zK7LZ-+Ejh7{^w=zyI`bBLi8V0d1o&X?c_I z|G87*4=DvcxPX|J(+-_5Whr6TfY{Yj%ND;Pdvoi1@WZddw>}xgu>Jq8>-o=-kqMHy z*G+r1S~arc<=!0v4)-d~xt`lq&yfC9e=hfLp8_3-Y9cHx+%^-U291t^|5H{h2qkSc z`Jj#6DllA?yGLjyd}a9Wi~d~-iO~jj;U(iz1J9&HYZNtJg@E%wIb$lVZzn|9!%*p; zUN}O=&?Pk4+~@?pc01q2SB|B1WCN@IGRsDQ_n3q-a;9Se-*K%|IIcUBLNg znyuU!fAazVb!Y%#egA$Vsy_}h{wfs5-tVPt(v}uXO~4_z6){J7<#HI_L>B%JUywC& zi9hP4_eAwuJN4{Ko&0?2x9X~0BQffwjvaApGHJR=K4h?6At>0l}i)lQuyDp9(hF+*G*>_@gz#z@R~-d1&vhCUG0#6c+?f9kqy}9Lw8thW8k1U3Uw=oh1npvcg{V^N)U!wVKE147si@P~Bxsr0NEVU!89ryVO zpc?90k9!3osD^;fNnDPNW&c7p)Bj%Pl{(|VoNym1bAqywMbiqJg&iKQI?pkNe|YMH z>%SDoMX;UMaE9WdnQ6Oi&L&y#lVF!%+e`oOAgXX+*6UtKB(7xft}tJtPCcOe!fzkK zWIum3kUD8u1zYLV3Z->DP5QC&m+_nn?BizZWF_(C!OFsisUc)ses- z?n|n_?gpAwVHfzu0pg=X3m^Blr1HZ5@T!7FfhxIIQy35lF<@JB;Nlw93u!bjTFn8H z=Ear2rr5tXYe~HUv2neSl&;Q^8`0uaa zziA`L7HgH?T){IqE~2^%r69m6)ylUiwtdbu{RbI6e;{-1{yo)Y@hfj8=~hovwLjlC zi-a9N_$S3~rtSp~FeAtu!j7)I0T&D$!vzc<{q0Tv`|aYcP-zkjaJ^cACgq7|Z1oqB zeS19}-~Hm@KS;CAJ22uEji|1heUOo9lzN(>M=%wEa(UMwN8(ma7lxSt3M1`G`QMuS z;8kE#a!@W$A5Ju^+Q8vLRZer26sUT1ujhY6OaC^YEL0CtZ%5*?rgH@^h0|XTq3DpC z{wHBvp=y%()NQ8e!hs{uf8?Kc?H`r|L<*!7Hfk5D-iAFr@;js67tSgD2eUnLjp*v5 zsklgMx2ta~mt(Nza`yn!Df;Nv|287P(jH!^TOmj;)KCG17ETtvwte`&Z(t@MXIS6GBMTnd9Pi z`_$r^z93iSpE~xpPn~jsce@`;I9O8Zs1gh?ufO;}kgQOr{l6nFz$~)dVj@@@iuXvE z*a?|RhZ_IGA@9wWnc|DQ+mLToa$jkAH2+HQ6NX8Z`hO5TAjdwUD{olG`(A+_4^VwD z_~G*4pB5WaO>m(qVk3@Gf0IYm^iBIee|5%{8wA*QIxIJ?d20(y{^M?5VWDIqfU2;E z`*%>-)GjT*_=jc6FqvP-5xg_bDm{V<5pKTW`@fX~h%uarK8$%UEB!SO{>z(#DF7+o z^kcfQUu0GCH>B(TIU>*5WC%D@)Tob@lxiL)%l+fSiTJ63W$~HpT|7M-Tef{@nVQ6c{wK*;c4c>nO<`*wHe`})@P@^aF?Au}1fi}#92-?2XD z>1MpHRh}n!rotX=Y44c|@FljqS$8wxwScgU z6Fpvg1zkTcvmS4KkMNW}Kt7xeJA%*s$Z!#cC*ae9lux_+y%UoX4To{N|4!Whdl^7S znOg>Mu9YunF>`mWDCs!Ll-LJRIEAIz+tx+q0J|hpeU)k|`pU8 z@|BiVx_RAx>d>ErV=ePMiL12JyS`&D(7s@6lT*B_)^aM=R-Z!*lJbmMWi(6l@$IYq zLt@K1T6#N^B?AIH>fD5he>;yd;lEUEG#>#nRzSM%tv@B3bP^FM@Jvo~2sG38>!2tu zdk`PS4@)>aKp(ZXF`=8m$RRiOLPZfw_)pZP{6%qOE=6I*N+>b z>rBw|-nY_8$^PaV19NWt5~$_XRH3FEJ+0lfr$=a40H6GzGAeP^%I7|JvB=m&*uPFb z74OBuvQcd-0}cTlQ;V$Kiu-KC*BLfzqu9dQg!2cKIl7{%YO!>ls4c}W>Q*o6l2^;h zMn}b-w8*8o+S6AJ>KsV3j0)A((32D}!#0CJtYdu>NaKzqp*Mx+Ln8;B!k8lRABO{} zb`eDI8KZ=-znNyH9>S-}uGeN0CO2m9i(#_a-hxyg4mSgT7YOCZQdXIo2<{qqPebym z+0JI9^koRZjFD*q_x)Jx$XAO`c}UV#C&_8`?1?~4F$+c3Y@K=GLAs$8p+rgrNdXdm z^o5G7L8x#k2|G4@LGoa%1DY!Hs=#!}=kI~f1tM22GYGNj-jhy4FL-zuy@zhffwKEx zrSPg{ES-1Mg2TVhZH(3hK#~!yLXnqXu0B)qdIg_&F+n)_<2+gVQ|`$f?Ng^g!HxYl zEo*xDDN)ncsVoik?lBhnk4xEs>W6_FN&lLDW!<<0L^+;1`*`GH{%lK3JyZLziejEU zABI_wm8AF4sS0DUX0z<7Mmj1(Vc1caWw&&Jcd$_^;FQHBmnc{!HEBV zGTjG)7YNN5Ok%5guo4(S`-tGd;i&_?X^5}Jt^dc@cSkjqb#Ds-ii(Pe3>`&4I#Q*B z1(Yhi6BH6UCORQCf+cpc z{hcgU>WIbufTS|XeE#u|V8B23X7PXbI8j{t4TGi!Lul##Gckni>`>x>Zl5g%E50H58P zB^8KVh=15|Loi%R`9Hf?Z5M=@e>EE{tvuQymXy(;W%DJeTmUA;kr%dxSZ};JBX9cJ z=(qhMxu#8qnXm{|Ou|gOYWaWQP$2EsNRUgrFi#lOm+H_su0OsLSjB3ApBCks)-3W$ zb^Gs#DQKdUCm+hXH#eUFu68mlST~_V6_$=Nd?^|QJ(hX>f2;q?YJjQk07_XbTsLzM zw<`EKZi7RSoRaW4*AWH`v*j)FxP*LQP}{;{x#X?G^xH}R_b3%$@H*<2`;}iO>aA(| zS1L&ih3}!U3_0-KjW0h#oppEQke7(|*%R)p@#OjC+7ciAECI?~E^Ol|;y!4j- zlH}p?cwW5sRwT^d7s1A0ivF^Hje=^2dPG@_o-#BIKbk$(@P+@?>_-1ZHgN!$Ja%V19YAOnt z`uIVpH>pt*(g(Mlt8r$`FPl2 zQ!gNL@)b?&qdTY6k754cKyN$K2ESkdvGBdKSGoa(2|#JWGEQ4Djn7D)9Ch$=b}n(6s{AaIduRXrV4;DxTIWLM*R3*JHIa)klU~e1^dOj>_#UqBZ;F_XQYl`WdG@>^#qEv&EKGd?39!kI2$i5aicv!>!g$@=6`8 znc9!nYEIm&?|HU9i~$%$d%Ol-pAzoOHo1REwr64JR$TosE;Ijj)#7^A&ID)94|ko# zAl0-8R-he+#BSMgo|I)U&?Ti;A419NniwLMijraau~e;Etl#3vSW#!~6J6Ib#d6#K zeE$>V1EkB2yc?l#2*juCc-gqWaI5+nYlSEBWVmsA+2pJ9k^HRzZy0&OD=d;xz<9YT z=F;eZX^*mAi}HsnzKc5b%asqfcQIW-CM#23Tb81BO0C&7TQ{*Wk)-SZ(B#)0YEqTs zgX@aQt;blwjqMca zjc=W4T|gI_%_?Emdk82b8^2xnOY}+l#Iwm7Ls=i_>hc5M{L4X53P2iwTez9ffUCl! zQnU>Q@qet8kdOs`?s3g_O`FoyiQHACByzsYK^Wfe48!N>b%s%nqMUBCU|4m`k0&_N zqvy>;lBHCW@-lDz9>stvaiP-F$w;*tBg%*Uw$DMI-4yd73tHaaVle_Wn@D*xzH$9r z!Tin?#dW9pxmsD99`&*wvVA<~+?-||+OU!eJ7)HRilA-y(x2pE`zbA9_|}a~Z;BVn z>-&Cg8`Rcwvp>S$$%dCYKkIDX=_VBNIi{5|J{#p?$Z4|t#(-*xaWyQ9<-8z!cdX8l z!Kk(L-Up{WHO8;4WjG0koezgL91$f-6$idN<1jb5KVJWGywdtP9X=tW^}Tl?glJ?- z1VHlcTImQu+~4dHUxL0i?L-6ZdMM#suqjO^$+_PX7I7vTZA>7_)bh@=&W&V+iDxy) zbZ^er%*#fX(muml?A&jo=ni8MQ+bulYiHXB=Ly$>jhP565f)Inh<9Zz5Cz}5&#|bP zG5E2!62M6lV;oX!WG5F9v|*5reeGo_cp>Y7a=CeVD)WETAZfc6WR}kQmMz<<%0}9Dk zL)f4Zz-(KQ(k4j$8>akjPt*=lb9$jS*>&Ti=*KPJ>E@lN)X7G(PLgNLMedFbDS~Pf zl5ze%gc$2^h1#Z^}vO`TP$HIfxNOxa!f8azayfC!NE;}18PlAtfG8V>iq1B|L+ zOjYkYn6&61yVOOA<6RUI{H52V@+pej@w(>p zRP!9rwRX`zOfxZM^G#mfeFKl0V#}&8g)U2#b8gce;r9wxrpv-vd}nDRF@WrOWZl%E z5+I24s1$p}(V;0dSNK#SZQMyB59p+@E;X|^?XKGw4Q!JU=tv&x@AV0gK0n?+OuM(a zbJOv!u?NTVTg_gC#5^;Mg3~qXR^nk=@lJ*d4-r=pu1zRj{UN*JtJI^W-q#Supx)@| z98GVVc8BBc-Oh}GU2Z)UNte6Se*j@SACkvNJVoAK>|*XSB!F@7kiVws5=4(V8D%r( zAz)f5{-7XtGKUNfXyi}GEWLc1m$*Rmc$Tu4mwLdRs!Y)&2Q`| z9(-^b`7j_;?F`PLK)aTs?R*yi^4ixI7pilUtFe>S-c<*Ct97Qz%zfCwf+}gxXzC*Y z3FQiHPAN2ggRp4}(ui$PQVNs){$y`w^0-1}r3RqG)w)bVC^x6Pj|1FL+OX^68c%R# zxks!9!-E-jd`BVVR*1N=a-#oU_B-&<>q#76$xNcN{7MYn96JzsiqNGJ?b!qf(}zHh z3A5E^;TKd}@$8oV!7=f%_rhwVM3<0mV$I2^b=D#0iHPlUE^&|L!Btr&#G| zcP)wb`Mz9v&oR4FI6lW)*3sb_Svg}B1OeZfa<$;ZtAkLvIsAq_z0&S;c(3W`j!u?q z3#tBb7W7C;ZmW>qZTH*JRM8$XXb)zzY7G=lE#$}~XQiYMs}*)|be!|t^s)dy{}J!M z`gjS|XjE%3zqM;Nb+&73cvLe*s=@Znv)Wo?PfbLDfsXzwBOP;Yrr(LTO6ox3YJPxT zTg0j>s|sxCJRHX7xD|x{+D@TZsp-iolc}t_ZYR?IuG)ED(!}<;L3dPGo0`Ygr)kaf z+wMki^!5(_%!d(&LshYfo4r7YDmfGVCVF+uAWW$D0DVucTod8h{p zd$`N|yIDci&?qWLm&Q#?Y!a?+$1d^77khL*d-I#4_l(qpEq7sK&eu4ZZQ|i3p|N-d zL(9$KHgC_P?nw6%6=^JHi;Uf8_c!NO+-#~YaN<10UY_P}JhX}5nXpyK#_|5plJ7qA zeK>)mcXCJZIw)C3|JZwx)U@m^fH)V=TiO$%%ZdTS}8GgTaAt`%#W8lVGhz9zL` zw%VjRR^!TJWI<@|vim>}xjWv>ZTI2Xl4$*EhU(ltgT+M;r`p4T$dKXr=Dh6{XO(aE zGx@mqSF$uak@4s`$6QnW$n{vK)EH;fP#;0s!wI8JYn&EGITb%ZRV}$5;K#a?Y`vN9{yv0&cS}~ zO$L*3b{H{HV*4A0d#xagtCIz4HS7NuZ_1ZfVQc2K8_d$!Ej<9J74OGVvEKW)RA3S* zrxz}i4Ul?OnkeK8<-LY|q@5$>H?De>Od6mr)A48MLdNegq5 z=?NuzZ@-rUCnX*s8BdsjpL3NZ_@z-lv+&)^0b0z$yE1@)68R3PsD0$oIca#2w~{mX zeO_*p9c3$lsm52|SG+Bo^Hyp7!M>qVXX)}1gJmVT)PA!4alfo>6Wp2d89^LHW+}0B zY43;Nal|Q24yW))&fd%K*U7qpVfEd0ooTevhI!gRx8&1}?bRbYO0xZ=O-r|S@5tnJ z83$~NSf>eD4*fh;qr3zt`}wQ)4Gn>e(q4(XtnIlike*M>rS)ns!nk6We$|!1c#yv{ zDB-<=m+=_t^9p^h<(%IxMfY_D1HaGnelaileYKe8TbYc?i)^ ztOhRsoF7%|$eIN0V=Ngj68~BpFjV4*$sTsRC^{Uo$X$`DihA|Rz6VE)6W3rakgA8b z3;VVzq`7?@sV27~rVj9Kh{JupucNER1H{v<^Mc^8z3^dm}iP)QS^Q&?CBjm*T%++l$xF zCJ)z0y|&rApvk>t6wt&~((x);f(w}}B2;31<6o-KK&^C{_W9!_ouFzIVnDkdmpNt3 zZuMMl^NK*V%i3xkabrpna}fq_hH=3MMh<@A!!LtQ$>%YpW^DWtHouElOMoY{!cRTfAKJoX}@@a zAI%@%;khd2Bg}t-(5}j^UQ1SfL4o+mVYDSoM_$^g(AL19dQ75_FE-k*YMHsnI4=x- z>58yK){J;y?QpSCoSJFt(`bWY|3*S7XWnD1u}d!d&`eU;CpYG+Hu0-N6$*Bhe)s&p z_V?GNNqqPbr{OfD_J$CGCwDoO+$03QG!Atcsr#O6p`2&F#iL_}O|}a!8XLBf7-&Ps zm6*-Vo4HQcaJHj!1{nY6urOA8G+Fg-lDcsevIG#f=6hb+8Qx&UwBJJ z@rC7?l}yMLi_tI|<($Ih#mGnfVT(7~mXjQ84F~aTG@KQl3A6@pa+}=^zZU5ETNM?` z>`CJ!mt|}aU~OD22u%=ZKuPI5Xt z65K*H27~s+LWV}EHofZGiuG#mLXdD_cGyP2_N~Kgy4ma>;fOij0t9E!2yJ^mhTwU_ z=Hn=JZlgwx3DZGCe{o|T_DX+(gc+OfG9w9T=*i2K&AE6BbD)fPjt5yLtL`?L$_gmR zr5c^r`X6oL|I5rooCM&qv=X%F{dI`!eO!k6=onBlsoGl9hI-p5Gnu%mog?SNp3MO}Xn zn_aGLwVnboOPL$=26;x*{)u{Ck>lH0$G_zB^h6}%hUUueY(0%wlzEz_eW<(Qzer2E zQ_nt(j>k9nzlWzCCZ1i4DffY25wc|26EOZ@_r{#?g8j1raUtLUktAB9y_zaIKXYb|p%Bb3r|As@djK`6I(}gMcULCVgbtZ_=25`i~6jMY{|j=k#{#<2dJiw_(5L3WcSQn>qgDo}ymuU!7)p9qZbH1z}@F0uNYYzba0*f znX!w?Rr|F?AP4bADpOi<~(?cB`y;Pitbku%I9kxcyI@fvf;Y9tgg0 zCGtnoW9u))(qA}aPUo(M1L*gGp`au9E4dfYJ;J;OYYI5C@8Ar{GZVOsg>gP7CRbr5h2qjIk57`|Q+BLLxO!Ql%XoOlmmzHi+4W+vxez1J)qsO- z;T%nHPSZY&NCwUObYCnMKVltTdvvtwJ(H2i7>UE(sc?=@XjS`{T124Ix6|2PUM)|? zEY<{hZ|bGcBbcltAwW2v>%>@ac^xc}nN(~r*UzD4ZR#G~>5VbHb61t;91f(oY~Niu zR|y?GV^aPkgStWBa+TYAiEH;>*fEWK*h2>(E|fLg9;goV#jIjJ)pN{z44@~a6Yj@%Hg?8WV&py9ulnzA-9+7R z5o=GWC#5IE25s{FRvV{j{k0{*#1%j_>%;_|VW4eSy0RgMeJXX#vTwG>y6$R$ZDVWO!@{BuC7N zjG21wLS`+4#KxWdL!CL4_fujwkFlbiaV0cS!+V{cbSW8>Zn_uS_qj6*t>ZMkFSvXS zW%6gL<0){kbZ# zo=F8KTVu&-IYfX|?-^wWXNbw}> z*49O7Y_;CKlb)JzPpP0}b>Y3zCgPXm4M6lmJ^hjCLz--@zq}f@mWB~E4}il~p~}k3 z`Ram{9#S*%Gq-WNGPfp-ZU{eBQlufbqJa>NSN`wCD|c>+{o zSfcXDlCFsZIr+Q3(|KUXkdX*Wlmdi8*OjhmaXhaMC+maE*SP_RAB9u2L$h_=*{LD# zj0_A+vP#5nWhl3sb6IpaRx$c#I})L|uNeal9F3 zMIS>W%VTRtBp`grj9MWM2+3@KftI`B_N@&6Mo%Ht2HUwTMH$v_U;2{!##|%@v&_7Z zG#-Z)+gK@M&e?`Ut7NU9B@IvC!d_@HyjP+|6Am5`k_!T=NhSOL*^&o6xHm3D4V|N1(&?E7)NGDfUNV40rmJ~-%p_8vC@iG|AM zRz^3>)1`6FB$zJ7YkZ*?IhCfB>K|Gq=$Sh1UN~25oeVR6Z5EyUD0Qm3f6gw_I5YP{ zU9R%w=0`>0`q5!;8&-M{oXX{KBCMHX`biSLTano8_An?p8VJ%o@a8o`bqi^)J!CGIVs5Zibf z!{|@Xd@#rqE<1mN(Is~(McOxv{`!>rP*?R)m~iuM;{#^TnWO!;3isbo4ie+_o;xgK zP9W}SQfn#w8emYI8YoqtZlDtDjH z{ezI9(D;3edionTu>*Jj44|_LFzg6t?R-GI!Zb=rD3_k#Hsqz=aAk}BPY}};dqR(6 zUQINYEe$E05#g29_K{MKGBf)23o9V;8!Pq>4-rVmnpk*t2lsslfdY?A%2Ez&JtNWG5x0ijg1^*mA04&Mu9S#QNYK?LKsh3X^*?OM- z%U@Q}Tb;QZE>t4Su*u71&#WiN)&7X#R~g6t^ep$YSmaGBduUwdzx?T0T2jh0m_U?F zz-;?|`kT6OIaTR=-0-Y#{hAQJ;D4t*{fB506ekD}y^PBgy~f^d_YAm@qEXQNjkX;( z_ol+FKTCISje$IiEkn5MqtpRiKt}O@Z>s-9)e(1rExVNB272CKNOXIS{7$`Pf6& z#-g3~uCbD#2c>6y_D?2!F&l((;acLn<(He68a^fdaf3}~q!#}%ZnD>`1~eCFGe4Yq zQdgQQw7BG*&+_XV>2HX2WGDT&#r{ubOrF84uXMzB+Rvck+|rgZ&*^uqs#|~a7ykPz zNztS{XDNhZ5l>TY$lYf6IG3fcl;-}|)E8Y@Ef4=gW(30`4d(|Yq6iQ6bEk8N@WSa5 zB5Se>pRm|8f;-I6f;q-2e=ocr{gb3L=JhWy6`;%jn5E%b%PWZnxx%Rlr;P_#x7L@v zH2HV~4@>wfpI?Xk^=bVW`zQIZYW?d~ucfMm|0XOECkv-5mbh6d!2!aqMfOM2h%wdi zceGYZEoL|VZ|uSUnVRO+h$Gh2Tss4?TOnoC=#G2=;aFYwO&3*X_;%4Yq)mn4=K`ED zZaWtig$7@vhkVk%8PZ;jA3KGAM|hwLTk#QK|yhxPLKzhZy2WmX54CL84Hh}hgDb}`B`*RoY*{OzqJ674G^>|k)l$QQ+f=l|0mT)d%AwqpJa3QiI| z8Y<14IQTxP5p3t8<}4Ln*brzEl08)H_`S|W%QWa#JgCEW$m2gW-t;vJjRRh%=awGr zJ+Qg*V`qkh*lM>Lf6Kk__lAEu8c*TJ^yqT-+u967Po-$ja#o`6vkqLZEQt2Y9M1d4NAt(z3tSq{I%DxI~1dhg^s(x zVB?hYE>)n;^U$iz67rj}*qwW7cl&9N5eJJWe}ungad5sHA2!8sTv#y%{dD_4XI*+b z_|lbO)GbdL$&x~xpOrPQBZgUo2X6(YSuE{XYNZo!AXg z*ZA8tt?~S3!M&G1lR3(K3S`vDK?%z!MsL5UJF3EjF1U<%YGtvYKhlH@u`{_hsSYa8 zT_2)#ED1HLXKc>>S5~4iKu`CE=0KBj{J5Gkj4TaP%TVrrcX~WIZ8wu|U|(PDK3nlG zkeXwrkfHadRn-zqczt$c>Ag&RhtfGl9O9NE5D zC(PwMdB43-@|}RiKl%awrW|hWk@j9+_9xeU>a2JkbKjKXQz{_TamWYRq*YLK4pwE0 z|9;Y3fHOa#?-s#uj!HvtTYYRHU8ax}G?O$qMm0yd9VvK7!PK-o@K9QVqnNUldQs+| zk5EDbPpIV124Is?`Yt6dx#a!!^JAQ3>?Yg^tu=t*h zlSR6OHw#NnPa(zr`Ds90#$Dn#QH!{iXGWlTn_{R2COtBYVnQM7@#dxGus)6K2X-5)Y6B4krVp# z8YTKM4bFu>2_LZ&D?4GR-w z-XBv)CH;^82(t%V>cV`agIz?0 z^Wf|tTixck)Hv6dz9#j|xAAvn{(`d`=YkNmo6>3cQRVOq^kh^t09310k&cq$2Z zVe+2l@fV-%C07G;)>8IIWvkj)SLB>YQMp19%lRy-%kifYOZ!jYv`f$ghZp9N2*E%b4SxdI zB~HebMRfyu?zy&Wa}q6R{+wrGsP>njpBBeUJ(5;O?0C^oKJ9m6KU-tj{DqZgIRd7& zL~;3dgr6QvExz8QE~tvZj6?**-BH#CM75CQ1WkiRM!+cXelLr=>y}%YQs!{h-amT< z)(qQ@a3yDBjkDTQ(gp|$&Oz?u_?o(sY|r0q@eGJmbj8?VUP`dR108?^8FY>UH&8&J zF|eh7Bz9=nRlGM>i!=#y{`r@#lsM$KKt}2-r)B3M;(0YY_GfWOsH@o>{O{-Javrer z-jNzB{(W=gI}U%2;G3b!Z9=Z%~ge_6`xgSOrq7sd6i$|x3Dn|&vYvYEZY{QjWo zyTE}=wbn3i5vDEJE#}6MJTv$7SUaqk!n){)L|nk{qq%=!Gg1%3fk< z*dFbRLlYN8M|Jr4p0l*=?>*TZ$3ORtD&r!6ukA<#WzmbTn|TY%ez47R|H4V>WA9%6We*xkidpb9R6o1OE$wB2 zAHBA}cd!P<-5@c~i!7IgxBt_qrAzl@>a=EciG=h{j-go2PbZdS%0Z|a+mjja)T@i! z{GB=e@cz#gWeT!t%S!sm_`a{%=6y$efHtzAx_6QMCrnd%bY>)NArgtZ-dBy_=KU!2 zyGa6zL?U)nl~I`f+Lp?T_o3F}I{_MpZ z`xT>vBhnYrkW;(97CpSWN(;Pc`V8eI|5rRc!n@}OlzxA6xhpreKO9{#^XIr26BQM8 zWjsIzsO$v`&J5G&l`RW8X_CM#DMSl?{$QHHaLlHEL3nwaD5IdXDE(jW3k1(+ zq=o{{$E~gU&)d^gYko5MBU+Sjnj>3|BOZ5!fW$Pv8F1v3P}DXu`;N7WZk39PV$?6| zmo8p**SiW#9sg(Wq6LCu^I2V!^ocCJpAWmpV>SI`m=*9stVSOqL!H8$vmJf{>(nzZ zds*Hx?N+iyjp`3`Wuh*;-ChG51`n$q3iOGeh6IF-ciQU3=gsKf_VW!QCgW9u=vmlOyX;9W(EyJwxw~IhXye|T+7mk;Xzx!^sQ*V{ zQfYniXW8K?MWZc_MqJ|~*r6^Qum6#!wfA{@>3^Rbz<_EUd z_748ja;o%$yq>Kj7E5GWCF25iCurjI%f9q7Bin@ISsCK+!!&fsLKpWOQA`rE4Ms_Y z{oZs5XzXePEk^o@{3Rk`tI!;#+LuWRT5SgD5iW7);o8fmlqBD$=eo`1h6bb%k z^Ia5aFUVnj4v8`JJQiXeZoT_zd#(G_=Iu%tqVy`r)Qe-5p*5t{pD!D^s6Wa3-_QI7 z;FMZ`9i7`h^MnMOPXyfvq5)>?d0@k^)p}VkHq-u`iGJn*hv#Kw z9QL*Yzca~kSJHnCcpKcS*i~tgRaAL zmUS^+fz^u=K9xWh!8%;;YA4e!pb+oAs{Q`AFxRL3GY-cv)BQT}mvh>x3(^oF>`lV_ zh63?5j_CXGr5512NtK>aI%Kpbe$2#wP*k{it!ZdzXxhZG3Q}{hf|0sM^xdUZ?T%Ds zr}J*M?mWNueN!7utS%6BpT`X5xq=v*cN8j*k4~wrD)@8GWf)Bv{jCK6wcecXbHggS z#K8{t5njT?)TX^J*o{#hc0?2)AvU{`HCx^h9&<=gY?Fa}d%m|Z)wtRfk7_AbFyIu7hBxdW!h029pEo&uIPHd;0%DEE~R_TE-H2LtP<>hnbqREt=zt_zHz4j^Ku!Qt{dZ)3^|O5?ny zy^F4{T6%9(gn-u~GPvz(v@HE#Z<`LjZ@M8AkDX&%zlC6cP6T*wR@U%JA_y6QR5&zV z0m{Ye@hikJ^9sL2bKhZbr3cn<30=~NQ*gLo(&+h~`nx=ju-s1Z!3>cdvCfln+5RLm zU`mQX8YivdS?l?Cb4rd|jh3@}UJQWtqQCY2W;3V7K|vN?8%eJ0n}cF$NLv^y&}i=6 zzK{*{pNNnHEXHWd=KZdU&iAo&+6tPcbuP{|B@KmYY(6uJqI=z)bCDrz5&WeMXr{>= z&L+b$ORfd&6pq8*(}koHSt{(oEkY;)O#r+{s5r)ZAwa5dt-0|oZHgmc zf8SZuX7OwOkA-BVi7@>5+|()d*i?Dn2OJk8Fd%;um%Z_{xeKj_vHHnMquGekh^Mxp zbzx?;&^HH57g}q`AGs#zSoa5_OOBg9p8>X`Q!)03ww$yZ8>_SD4r|?mGzI@_-XuM8 z7^UbQ&nV-meejND%l!^e8f!%O!pm!AqyM~EkzB~fDGoaF!LpRw{I2`8c44rKb1t3S>hpI3>!T228+5RfJ>UkaHHABb#V5gU;rUt4 zcHt81vVr|Bu#u(n`-?!$XLn|_A1+%G3DA26^&M28aA=Rc=#v=^6YE^akPgIiO??Lo zePtV}XeI^jFHS^P9i~xkE<8wlP9GVjH1NJ$&H>vJXnZ?%-TGc|16V?1Ze5HRrD?*v6>PnV|bakl~In&E& zNrI@?E`8OZt^yXtV$*S0iXTt~@5{vc4i67M(9|TpTk1`g?OgU>o$|M!S5s3fUZP|gRbuaD-Jkod@7ZoNPETA9m)}Sa zo$_U8tlwvITT8f;lbQL^BnG&{bk!;K77rn$f^GCWwq7xv-k(gIbDQL>nHPI78+ymj zt;;GP`*T_UnJ%8HT`3ESKmE~LpeB-_%JBB@hRShWM$B)+nGjO2(Uu?QT4cJ_-O1B1W)hPLc zUDU=$cVySNYx7mTRDaV{Z=Xt%)UFn@S6qtNx*YkV_g5S^_y^n;BWq<-df%Cmli?oPus$KH$(#Cye7 zUI$hYRz||C^#z`x2i2*4E}SYonfK0u@kzr`c6Tv+VkMbW6U1%yi1H(IoZVzYP0AsD zvsl%`qeer5^JA*tcOCED_I-_+lByIRB4hn68V`<(Jn6@uPA4cC+A}GO?Egh4t1kQG z3MTL|;`JU+;+}i#bgMCsm-#~%&EWh=Mr##+d5`zP8>XAE!`mfxCVzg$-cE}2PMRaRE|gTY>~di?#8M5>#T9(L31J*F*wiS_F& z0Sn%rx+Y5w)BN;7Ubywmg}&JL%#yFEdZ03&pt_mqW_%uYBDk*|;StHOLs@K7S<8kl zUx*G>P&MTNOw3JvdW=T7Tp#B8;~Z?M!jRQIt^V1CuScBg=ym$Mo~=RHOLC z`i4TpI-YiZMrW1%sXYeB(*p&I)ugZ2NINik`W&0w$TH&8YBTdN51gAY<+~EvQw6G) z;=+tYYrjd!<}*Cd_aosDP?nTq!H-v$$1mXSSGw07~-8*i#o} z{yr4Ph$d3?6i|eb+5QGfy^wwRV9*B;(*I|Uj-;y(z{^pIZ!Y%*@XIxhb0}oqXGmj< z!uF>M9Zo4p=P7~JfZaC+=mq}BicU7cIdCI-@rhyOROrF(S~RjmpmmzZI$T<<=TeLZ zo0RpulSvblR0)Zu>xA(yAN0FbjQ4M{dUgd3*6ZbsTzgvP2(+8jNKMtQ+yjVYH+*r=LE!=LI#1?K0VroIV_OE4Hd%L3O3! zu=W@^WznYkwE6-|z~k!?Gb<~)H%zlH=j&vxtPlt5{0k1e`CZ>{)K(8!WcXtRFXw$W zoiLjaY!|#J;ME3w@u#dh?3z!))!41WU9RA_D*+H!Us*44@*~;TsPoq z$x_ZS7)w^ZG}uALmh6Igfo-m7sP_GcImA(%qOFFQqT*dDbI)Ml`ei{;ypupxiEA^| zLS4Od(DSTn_DXhA6Nl!d?^<2f3vnAwRjF*BheFuSdHx=CPLnFW=jA_$SDE^h>$yq~ z#YWhBE|gA+dVbFiB)SjmE$r?Z6hBn<#7_xsGJq+*p6BL4&=`RdO9bo21=;--ZUa+D z9AYh=b_bJDwr({=PWuU;in?IBFkEV(uH}f5G21^9CEz9(>V;`uO~O#yM}>Q{nu$jt zq21N_x&@EJ$jX)aOeI`}Fc$vx`bD~*7i0+SX;lkdan5hh8B}f0HK!I9JY9T<(iClO zC;5WF28D*PbyNHIExq^dCCb{?9_)3V<29=-hT_o{Y%plV1Zlo`(X_RDB@k454EticMygpbD!fuu{x~>H^g$aV+XQewMV6O4wEWE^;>TB2R%iH zwPwbN+sxuIb$$N+M@y=r1dNUwZsevmil#5WZlB%9*oUx;@3uZg#zTRNw34Q5KMez) zi$ZyrSZ5B-zC_&3b-xeD>Br4x@ys4b>TFE<#rK+}#@*?|jpk&c?v+KlV@Hu!PyHkO zlh8m4sXP!-5hgdt@mF`NQlG!~*gMPM`y6CyyAfkgcB-vuJf+SmlJP0pYlz3DtqrT9% z<6MUMV%-I@(r3@;7a3YudgC=6)jIGG9_WsTJ_`-)O$gd4at+uzn}wyDgd*^e>zI;C zYu~63llHnrhJtBr&l*uV@%BrNYOP|9;FpPRBGhPrw3`X{#s z*rh!9?sdO{*Pp6^C9hubr%bNK_p;l!r>gy_WA)@=TfKGZqt7)6Y` zH1#iZY)lAAdM}F?eYm~1;nwE&y4mxPYP8zBXc*?Q02Wl2ie@^xU-1N6RB3>&Q)mSTwsIs4UpQn7 zqdpP)NusqW?Zuu})Yj_7tFGZY{en&Y&SodAh%A4)`-?K<^R3wHJQUEo)>v?cQQ7+Fe)HBXE}J|)6yxw%Ihoq!xL8HHLz zAS*@H{5KRxdF$R9!snFTlR~3+_m?n|g9rUqc4yXeoE^rQY&d^UQU)s7QLjyVgrtVR z#${qqulmevCcpG4d+8ueT>Ir?3xE#!!HPS+MQ0r`&Jj=P0-}Xmj#Qu)BAO?KaKi7I zCCUjW_pIF@=|zU`mj^4o#g-<;tZl}==LEbfU82``+m`~vFX*(I2GtiYgh}d&FE7>% zK7DHPZo_}#tK(fww_N2G<{x)|ra{W-%U@VEY)@SnXXeWeQ)LSOkZ`)b85A=yAZ)s9 z@FrrB{D7)>0T?I@XLYVAaK=jf%j%nwXRzi_3l^pOWMN(I-n~47icK%Q;x`B4(TMav zML5{&rWZGOu22HizwvaI{(A%{c6S8Bz~d3B1Y}{QfW!Z z$Gf9Vf;v|OOs~OZJ=bEKH8oJv2K*On-zlA-fJ@usF-wNYGS`aC;8#83N^!yqESx_uXXI)c1Y<1ba$whnbmV$rkUj@rm}Cs!YZ})!CSXBmCYK&od@ra z)!4H_PYCqkuf#LTL-OE_o`_07Kx8>IT92>RVRS;s)21j3x5Bt<7v|h5=9FI#f)&r6 z2C5wZq-x`({R*PlqBl~zE-CYo$l6zesF~+nf`$;2U%`-XB0+m6W-pVB=zrtD+xvmq zg{;rR#M8n)Z0>izz2m>(QtKXqy58r9H&KNysq1Mei>%IIf+4fI@u-u`H|PDEtq3P+ zSmOANv>yL>%(eYQgePw1)g%T?0#VPPq=4Zog7r3mtxl{4N5YR$zN>=`ZN2@C| zv1;zXo#@@OF;?G8u^M+!qd~xp^P_D0V_?n5m|;guccnp5?PVu3IEOvC0Vl9avsg2hhgn zjS4DHJiKiwG^NQS{iLO zJ7PNhy?A%`S^X-(%w3L?ug98X`GkyQ~tmi=5`L@y)<|P75&i`m_ zNzFxBB=0$B*`V2h-jPo!z%yv}eWHkoVVRk$0sd?4d)!F9dhO~MmtHP1;)*6S!k$~W z4vHIjnt0Q()_`l!kM-b%wgS>nesAp&zjodss;05Q3ma9UrNzjQ>RT)ET;n9PV|rf+ zfL?q9Is=(m9UI9$i6@L!*=(O_g};3p$rsD7R-g+}$h)RCz}=RkcPr{QGy9)^Jq`Ye z0O2C9{k>NvKo>R6;tOa?>@S?PpN(J0q#QA+hm?e-iH|yD09)gRpur?9QSEep#Y|PB zPws!mx`NlNYjP@i5xjjVVyaQW_KQ0*>v8ay?hd1EFVJJ-*;g*0qs0;W3x}@(HlU80}v(1@jzUWM)#RMi8E1k z>IqG(#850$hcl#jd7}CLoOAeJdPCqzl1Eb-=Jt4B_(Xk5Sp?r0-ChRwCG?DWFuPsf zkm^fqUcmrkXo@$5GE4#Kdyn-^Ogvqw|Z>0$6#BEPX2SCZvYvrwY|mas*@+pG_^exCE}&YsSrq(AX4EG!ON)^dBkA9TZ6V@3mmJQKXt ziIYY**d7rF^OgMzEQ)elukj=QA7yVD5Y^tb4+8=cM??V;1Sx3=DWwtVls&XF8y;N4^0=RTh2dH(M=z5sjw_WG^5u5~SoU}%GAor0yp zr_VC8OB38U|1{$N)PSVfXmkF!@5nY2@tB|^_hChMB2PY%{cuH#Ssk}mx8{(J%3AZd zNo;4QT-H;BJ1mvY);j-YuUPgv@GOZdp+xbQx-Ku(6^-Pj=v^%}+Rzr;QcCCP6N=)u zvpU#}JB1oAbtfgN6x4wP_sBboA$Ar9PqRnw<@8}GQyvZLu;i7upy)cb(X%EExUK5y z>WgD_vt2u9g#DtjxQ5UDqn1ctKqX$RnLa7$@VS*8A@t^K7Ry_CAuhWTJu#+pTY!L%+e-o`hIo#}+z+KqXZrx!0g+r1FZl#oc)dSwtR_!E^sESN@BMPz9V)xJMOb9R*T#Mdp0aj z9(zbUuAB9OB8)6SvKH_Wu#w{yjqn%!d>S!y{0+*!F?g+THs|q{83J6UR@fl~v4(oB zIV(vn#fu*|32n_J=?FC9(Gp8yd9?OWezdv3&4Gm`qgJ1CUsoDGAI&v+#@f$HAA0&m z#J^R4TvN7VLvdqGr~uk`QsuYldz$x~-Jy7v*WPDH1R(v7xsz_3&1}l~^5& zpia5Ske|wVz|FS8sltWv&F67#l!z>kOZsRgS2w?PFTf7>>>Oa_pZX{EHNRNMc$Yx0 z)i@w0CM{3!R|IWknCCjm3wgaSP2Br|6fXSG55iH3Na+73LGW=sRs;sSp{o9G#5i1v z7EmAba}GAGm3r<(fq7$gAbR#6zm_h9_L7quP{QBpfUWpJO~EFI{P@9>*MFq5e|K1K zbMb+G?q(PXhHFI9tMAz^F-sd(5H8G~9{5Mt2EU&Bclc+K|DF#`t(~lq{$_Zxm}3HU zb~en?yljuG^X*>gL++AD$!+q!KT={0tao<+ocLYALyTTJ-BjSzIZrfk$|+Oe^$Rh>8;x84*DmZ2D!lW|jE|p#6n`n3_5f(Xaoo z!rC9zbDx&^PrbDXJZ`Msce06@nahuR_|S^I4;WgzU-s7zv;7kl|KWB>n=woawYPP@Ql;jX^MpYG8H(st@l{hyqW!sP};m<0M$WwL0R*M zN}V&gDR%NB)dxI07b7Hsqm1ac|KogrCt3<i^jj0>;VJt1t@g742$2tgWLS6X!mymp=AavO0X;DPh$1opNBXJ zZ4^HC5t#omqu(#`_dkabWC$=6mx487vTJ}v$2dC+|C<=H1VDPhZrx=Eb$F?%4K6>F z`d?=V=3ir|QOfE{4-XGh*``?jFy?=M3V^Xk0*LyisbF>wkECE#V)&*58m0vjfmsFx z7#WIxn9ld$8OVTs$tPrCQQ;ni7XSOlBklwwCUhNza9CU$jBp;LY5ea`v&#9d1jJ{^MHzC+MMniozq#a|R@tc?k4Ft$p_rs&7CwRtFfIhC5EW~YBb0tS}S^wRl@h*SQ-|`9ztwZ(m z|D0p|`*#ZZjJA<{jlim!ni{P1zM{XI+HWiVeJICdrpfC5OS=!aL`6lTDgSnO0JOb5 z(`W&}&2j1JcTXDiD>Ob%tt0?7$ohCWmln|S>E3qb_~U``B!O>7tgHE->xritOB%YCT;vrL!mWKSU%j2cq@4PK8L#;}5$bO*^fnn_sJgCV z%1XUHwyb;6yOC(c@;ThR<_Gj44W62rF|x5K&@=E_VqF!$$R9E6Xm6L7lnlPZ z$SD7kfGk%bkqs01GW?Izc+7`3OqZyLi0gIJTaP!sySATEzOWTg!~xT4`T90}{P^*8 z)cG(*GBq|~4K<)Vt0)^}a|@u&OcT{yS^vXROAG-3jce!RmPblbANO~DDlrol#fZ<2 zE4$Kdomh9*y(Eq*&p9!h(TaDP+!`IR#CWnnIkM3T&E5yOZfnEZ7`;B&x)sPAOm0+M zyy(lXWk!v`4#6jXK0h`Vg)5A-?PWdo&x3i9)oPU$9V3kJk^uI-Q~GBVbl{m2SXU^7 znB*xWR$=LQVD#R`xzVdVFm_nO;V?qXdx-KD z^Q`vn(@zn0%Dih>BXx+F<0|m9=E@LuPvA5XQjDNz|B`Wt5U1sW@$}j*wD@etIWI7G zmkPQ9TgM`YYu)YEfMHO#jj47a>1mWhw`K~=^qo}d)iPi(l)-b(x$$EMXva}q#vc`V z)@HVt zQv^^~Qv`i#?;5m7UjMp~=U$TJ(&11iH(6!jcVbOrKh-2{*_X*cc!M+UW9}WB&xNu3 z>yt(&S)OG^?NiGuLR^1Qu!Ry)Cy}x1=sQH*JccN7-|+B0ZeMZXtJpZ2ew7^!^N;%{ zlLzf{iD$8nDe_5I1slfpbZ*EQ!-Zx$W=~Hvo1(Y;d^U{AygXV&=~aGZpc^{=5T!bg z21vl7?t0bxv!qI!EHn)g`_5a;qtb^*H8p(f1DvjnGw{kpr|$iF+e8J+J0YkXr7Fm^ z!FH6UexqZC$h{*QlA|em9Cyn5wk)i&QJt?M)9!RfWIjKcQ|*3<8mQ~1;&p5i+51cg ze@$>@`Ocj?ovHkGi-TWC*IeN#yWKJ5;A`ZE7IAU4J<=c})-6>#E1a&DoA=aH`5kO= zLiir!=kSUh&l{DTG+#Y`#HL-ylE|jT_b@|C+c@XdmBqAJlcUlvysKetyQiZzyPqm_ z%#RG5dTx^(4P$7z?9!3wPMekvFP2R^kzJ{`P;NU6n1LR+99+4th~$MTtVB^Sfj#Fy zCkN{(ke5%TE=i}_j+N5Q`Sa5oyxks1sb0(qxqbh5$Hn{fP`$-hP!HlA={_rnI_Xv% z5Z|CkiObsf?2q)*#&7{3ahC^-?M>pvaw$A{qIG8rbtb1GgsKlYh2O2mD}q=Ft_AV! zxhluwmhZ)%pKNg1OjhL`cuEFvPH5a*F5oy_DLg+L&B|a`j;S|l^fu{|)Cn_?* zt&pvpM2(xBiZ#`F@OQz4$VfJe!vS60Qavww#CR?zxqCoebw12&+an zBe#T73%HaI;XZRFnOcIOC+FQw)73p8UDX3S+ix;foNif*ZWzv$51-maHTmMNhLY%s z2lnecN=KW*i^{;Br8wpbR0P=>bnVr~V%sU=AmZy(vsE;|cL`A4!>Rj^T=UOXUV?h9 z%W!$@3nv8%ot9{XJ(OOZA+?U(H=lcLzogsU90{ooLkm6<(TLp@_d|-4a2K-{Ekn&d zMa8d}P_l(&`D;fd2Tr$MMu62(+Mm6UciE99H@DjZglrRxMZG&o zLT<;_*FPvRlubfy>@Y9osEUJx^5@7+Cgxm?AD+PWf#%_KtrW%aWXEu}w5uwO3FS#K z|HYV@8y&7=G`jIGR-c|s*^*2BO_lBMO}K$BNMBdq}N>x z^-XkR)GEtIP3Be)^KBo^lYOy~#)h|tAKI7;Z)NlRj!NjO_rsM+$PuQW6$i+9@}3ES zG954LeJDrl-vsgumb*b&{uB<8`|DGA@n?H9SG|Kxrdpy{CRYj90MZC@OJ($?e8dm8^LXykY0Wj*(M9TkPye z1EEohGA)k>2Vt4N2vNm!jQ-awO;QQS240I2L%*l{%z(ZvNWaeYAW>)0+dxRtE(G6x zEma4sU)!mzBRgj7zlOmcFuA)l+1>m+@YdZDFIoUMSR?!;0Xs%np1|K$^Fp|P9{H>uCXVx8WfcE|0viA>RBv#xurs;9TNcWU zcbPn%#%4eStUiJ9-=;W<;ipt4{LtQkh>ZESs^&-O;cbr%qYBOg@=82*Odl-;;fVx| zm8&TSjePA%-FQbwCFJBFHOpw|Zdne)M;Aq}#8ct>0=!3y<-OaA-Cr2AD@Rbs?GFH< z4E&)AAO9;?MZwF|xwU;F^>>n92n|$}^j|1x{jTGNjf~E!H7=Sq@;DC1P2t0s9bDn5dfX_l-Sedc{W^OavaN|q zh5|NAj`9c_41*MbH}nqo?LwCl+>Gui_oebz)=E!a;2vxkjF&pdjERb^s@cI<&gGE2 z#SQw9Vwd(M(6ZNY+DPA^A2d_1s9(DoR<$03FF6t$g$|X%KrzUgQ z*A)}PV*y?0@Uk{C8WSc18baP2-({96vJ!2@IWeoHlC`g!@m$0z|KxO`bE;>Cv`E1m zDwqg4OBfczjfSPkt|A^!JLu-UHm)lHODlN*Y&Dfs3#{=78YEdAYEA>ZpPEcGHM{F_ ze-FIv-omGzZBd_6perY=6GbJ+_Td%G0$#U^5*Bqo`71?W;p))5YRL_bD#(J=C%)$8 zeR1DZj|M}Y&4c?(tCeQMmA>ZyPq3#2b!xF%^5M?dKC7L+BbFO1;)LJM zqb2IrXt`C!eLMK3{QzS!?wb--Zro8(2o`kv$&)fa-HELXed@A5$mcsGKtI52fG;2? zm1;xfHD8mt57hUfPBEK-VX;?6&E;XWoQ;0_#VQ!nB(uU>bbVRf-v{QQ3XL#`XW|v8 z2MbJ(zK$V+5Zo9+?G$90uaDaJmlvc!qtqfP`ExD%D2JPUJb`3PEy^7*M$Fh^t>R9D z@~iQUhyc0UR7bDVeflSdp<0B3F8j&7X)uW!och3y_-EyxJ@%A&zP-C_ej1g*<8g*P z*<|r%f6Xud&Yh1v!?5$D9TH}FcV30fjQb{_`uckMcVl9r$D<>|<*GC`0s02=Dq?pu z@|8hAve&h?r5fLHe(+jVJiO}S{@nBCUIEPZdkajRyJRIwG(L)3QxNmjWH`s*3|l>8QxVg@q$ z#gjud7mMk_z>~xgtGd2o8M*3WHT8n@PLL(`#MOWXLMn{uDvOf4x-~3s{hLqEPc5_F=8uZOT4+!w*>z3VR0G}P=jSm!lYb#vsepEA1!tkW9Zp82Cwv| zMFEsMxf!Ut`F7LsZvQEbLaLr;vCP4`%McC*EuN*9@o z@K>k~R?smNkhoosxE&UDh+?i`Ii+zZLw#v<>OzX2-(_ey(HA>DBjl}Od?an;)O0S? zuyJTWct!`SE!4Q%B6`N}^`!GLiM;_U!vzxTPIsjOhXjMYOpIdvjA9)tmdUz=LDAh{ zFxKGk7}_VK&p%4NO7lDX|MX~#^0eY6gje^|Kd&FN6W-ynqehq^{QO?cb{w`eA8?KX z`=@V<JpwzS8CFBmNpZ?@g1yrq;$Ih&?kY?E++cj+;R_spohl13)L7U(O}L!E>}+ zSN$g0e2`n7rS3sz+IOm@@F!^cz2|Ch<2HR~)+h|S%)evs#0=vA6p8Kbe{#LE68&y@Jnkj$HRVQH~%5zYLDz!Cy`qxH^M z21|#kkn2h&fUV0$-%k9Py9H-ysIur0`sBJgNH+H$q7gUe{eG{=ykcS|KB6pl)Llg<*g00-L%URC&se~ zg1XyY9&LW{0-G1-sicH^Mww>562*`XcM%Xupl&t*9HquR)1zZ_rHo8F5d7d|kuWhn zTuR~s1yr(it8lY>s@__KnLQ?Ve}G^NTvD z448HpG#EuE9|m2!?okbFcupKqv9p?is1VBhj7vd-q=VPqP`W8ADDW^S#!H9j2<`HV zbBXFq57yT71xLO5OZlGn%fDvM>VhA@9WG^&>3eYdiHmLB=zFG^Gr7wT2#r*!WJZq797 zTqhwB9l4C_>Uv1}8bG6BO|W6XLphxPyt&dqW3iXc2{dgXri(v==>bzoravXx1QGUc zGZ+aki?nCnANc%I(fqTMMqb?Uje}Mn$30HA%2Q#_&6@3I{V`iHT-Uyu*BbdMiMC_K zUDS2QbpQ*i!1rWw1@3i;<|H)57+iDL=mZpz#y#6TUGE$Z$>8-YqZ0D%oEjHr(=NTk zxwqxoH(lqM#e9+Yq+H&uhVvmP7T3@>tj2b1SgZ!_s$XFptCY;GhE|GXwe(}$@@(|j zT8w+cmC&77@_gMJ(R4a{>cV~e)=zJ&FK^DQQOu=ba8(zQRv--3WP4`DCssy@K) zARD!{e`xZ~l-Uh2{P2@f>QkJWn!Ki9Q-E*3kvY^5;;7&|Rbx|q*BJWI=v%oopBF|&Ax&<(g+2mMg9y0t&Ya=NCy}!*Kz0OV2PoXA-H|1Kzy94)_QUszTwQYkT)a8eoN>3xV6!8J zN=h{cn{G34V19t=j;2Pz&OWm0$&t4YJEB@^uR?vR_sFfGt})2!k1+TH9CW~KF{tVRt!VSk zH(cF=I>xbC0ThgvX#A!8&v$$@?w>}#l72(pQ*m*w62^G3_!}t?Sgg1GRTU0y{qT->8C0&AWsR*^!(P- zSdRSc1xg93tl6BcHUQemD@Kcxh^P4y`$YbeIM*!M+HZ?`>xYJg$$JR=i5LDpc$E~1 z!zZ^3O4-{t-ZL=~3v|uNac?z=@@MmJJ)w&{t5TDhG3=i0a~7WOiBNmmHriKNNlWPM zfuMV2>^+3gqx`1t1YbOH`G|LXl6KLB(C`Q8m>ib_^rw$bD2gTfH%BEy4DsvUDK6hWu*yLZ=afh*cD{73*5z_B4iHD!_D|-zy3!n zH|QG~8m=4QWMgZ3`bJMSSdijO-Q^1L@y=oR`IYFEBx#jlat9Q7LyFID?y*n}r z&oRH0Uq8p4Km1pSh(!k7+xJekv9Te7Y#LSrbdBb45(ZfM2CB&BIMuGKmb2;E&{IK= z&55ElDNY^H=PY`|yg8J4idH5xy{DtYDI1~GBF_hfr>n~~sN=$;C7`HJ-=KM`g)HR^ z9{>6;0o?u@8D9Alg(r)fh!0E8R$Fk}*mR1m-624@`ZBnrG1UQeUw>W(vqL%->C~02 z=z0zhyAr*~r&8G|+W7b|V+xXII9V0vyBf1O(V}4s@AM*_IHaBibJo}o4TqHtGX2js zEX~x4S3#dcltpMW|7Kptbby_emBE^njDi(#B*=8lN?#e`o%tYhRbzNYWSW3Q->zWP zn@C8M=Vq^$5qP3kNZ+}m-$-m_`Z8GG61zsb+{nSoVtV~`JH&2W?-;CK*R8FB`#=Bt zFL%&YdqKOsGf~?s-D+6GZHI$G<7Vh}gW`k&&qHs0ALMo{>h#cOcZ(VTyv5IsLi|=e z4SFJGhxr1|eYM3`-WO>Q+cXe1hn;xYoI!!uU_VR8i zcAA8089!fRWPg@><#Pa{S!|2XX-cXa#BP-@p#-`=>u&&LKcY*r06j%~K->u@T_nZd zW6C`0qfm1CUBE>hYCOtS@@?WPG#Ow^<9?D&C7?lVsXT7%2jO!Zee4$giTMI?ac8n- zCBw2mm9hM4z=v+{Z|A4z+GnGtm8s#tHh3a3OvrN;9jIj_N zw%8S_GaRaYHtRo?#N^OeG5pzUZ*kqOya%2h^?9n=D%$FfmYZ9`XXojy;SwFLEGT6% zbPH}^ew60a^&*VKV&`l7aFZ9Wp!Z^7seZLSJp=vHu~k;$LsgMAAWl34^xv0C%3(=~ zCE7jJ1EO3EGEoV(S|wfZk5XQTMFqLaX%VqTSH->so^4cifW1K`?hroyw)e(y#_a}GG_0Xx>=(IO^E$Cavo>GUu+b5m{2=4A+d`zvhlBgiJ%j$2 zSo2=}?K67fCte$FCz^PMLrmS+t%kb?!+i2Wi*bYoQt~+p$ApFs6Ch4Aod%1kS9}}V zHCyi$0D2l3^-?1L+Gvs%n@(aX#8z%EzfY-kRj5cUM+R;S-+_!9GVCxB9|D?)UHwLl zrJ%COu`=abUagciTKxzw56c$~6SvBnEhA}1hKx4u+t(PYsi|2ums4kA=nmW#T}%Xp zc2r4?G`=h&`Klm6rdF$hRDRoTejO9mDU&}( zpMKkI`$Jms*wBCSgsc|)>=Dxj2$#O}d0UK+2zYFMW@HV$stB>w0?#MIyw9hA z30UmmzN<8TUd%?a{r%(MsOzKP??{l$0o!bWx!s3?m`gx;@D>l~)ICv-^*=XafA2^- znOAk**c5Q#Kg884B_pHH#W8L+W9c`UQM0%8v*_x_-@LDJ<|GuuIZc$`b@*tlik;Eu zFs12I$M8~N%9V@c9;B!1innA>1O7*8Svi}%xg7%MYwAUAFErmo$D~>?W~NI_~`P@A_BK-V#n${La~o$FXk4A-8(J({0v_jfrUH00-wA zSncwyL~dSlB$(VzSAYOUi$?CRVG!0K@1lU=OQ4TBd}iXYRK}+V={SRVlphCx@jYVS z>%48k59cqTV&y27Qs}W=wh*(U{eeF}P5{E4FP2-w(B%U~azJfUMlX;j*rZ{>rnQ?$ zf9bx8u4D^#rtqD-|D-+Nf0CxJSa=4#E9chpb<1XVoT#pT6QNDsHP-Mt9*eRy-6<2? zlC8X1Vid`;_Lx^)Xe=s@693RMKVuWq;in4=Bmw?&`eOj3!%!;7q+8;^(3q7O{hhu1 zXs$ls$GU|q=qlx~;jt<)sAVKM5aL&Tw^~*l)F&2;XBa?)d05Jeo|2u}a zzjvX74AGJd-SHRiJ3CqA6&FNlh(G<(0!Vv$Oune}&yqY`#q{CR)HCQ5V?c_U*d1(_ z>w~SU_57UGw+*3&pGEYA#w;Bw(n~vk=0BB7kS;8#$&&Bc$=I-_a~g7x41AmnUTnTpZ-zda zpvN~%v%fy}7up9TW&i7WFtMqw-L#PL_p9p#22-v9G44`1-^C1JYiE%$)`{^jGNGmJ*9ZtP<@XGf$j0XE`W7+A zAMX+46oeDzNCD|)WGy{Tbb6$+&-E6;>gTlP6M@E-&}Lp>j7A{_XxV--mj3G{f_(As zFctJ9k2%UKSq_!^V!EhjVx7!(KabvNUlpk={Bdr99{~L#Dl8Sn1(3}T+S5P(NQB8o zT%PdFjCd{)6#`v}IkOYvUn4oKMd-EQF#+B}bNkM(|1=UjebXtKI;A@-*J^GtFRDt+ z*_~wm=XXiazw76wS@VeFZ`7S%UlW_50&w>JCW!0g@eu3}GBa3st3Jwet|znq`3q>V z-I&InzQ>;b_ULc1^V4D<^mtrD5fDUn-N9BYasRwrUr`68!|3iG~}SqUi{4N z`F*eW->!`W3&Gvl+t9Z17wXrBTP(Nd6jgXbUL8`)14N6pstao<_bV3CIg6kJ`MEc! zG#qV^{?7~d_yJ(U1ReJb*wxgAzIyC!W!iK9;ea1wW7#3|yW*(donGu32z~6FjLgAG zny|xqA`X7mwRpfOe`RKx;K*M=U)qmr-KWZC>2pXXW8cfuA3U*Ed9?dj%g=kcUDrzm z{gWU_YmS|SP#|2BP*a+(C_RMi~aGxqh;$J7M z37l}aLh8nU!dg1d|0`)Oy~^k7`|p29kdW#@66XfeOFG`AjFN{D(WjUwzoCMg&Ud6m z$0;c^9u;>>^gz7=?mB2N=ac5`exR@APd0=kz zog_|?dY%Gt^SkdHVF@)nd%+k^pWalxFX{WdFHy21;I|W))qYi;tOHx4cW==-4_$NV zC7VUAHoUuDm4lG?bJ8w+h*A1N1c)|h?>$fYgA52{l3~+*#Wh4be|xHm|ADPX_pfX{ z(#Y1@#c!0TL{NA5-4Q|CquhiP@V8_N?tBf#+HxuBNwD=7dK6zARnjh8U#8&FD>*LqYK8b}! zPrMJ-<=2Muz!=F$yb4nbK8$=5m#TutpCNWS70Qok&_@Y4qBtV5@>n$v<<<^iYW|)8 zMw)^84q&$N7^%lB8*0;R*CNWsT;qvlfbeEYpl zkPExI-6Aa{NjH3o!>60qRt^}NUJ$!5hGjGsp=HU6tq%Zf+gyIHaF_Ew z8a#ec*y_m3!}2-)m6sd1(3MiQCuF9V_-;9$*Ouy02xLIKA%OY|v$Y`Z7V98=EIXmDgvMJi@kp?b@ck*#CU3XHpU&nw0g! za|2jLd+c>l6I&B)6`$ADzsp?%@yjJu8!@`iR8cZJ&iILv!(tr1#OB@&E47(v;X7U~ ze~%xSm-66CY($&>bvsM7{o8qY3{~p9RyRidty7}ddcc{7W4FCLR99}UjZb*m%4AeI zcf8{(dYUh}f4gh!on@}b6B*xTZj@A1R^G&osku_W!HrBinG_%?V!Ryv(&HO1)m+-T z;tvpqmI@u84oCOvMhDOHdMXXB=DZ@LIve(JrzRgSMN0M7fQDwa|H^4BDEZ;p68^&U z`Ka>*8)kp+Z;VfXYfw>e`LE{8rP-Whk#jxgl)zC981+EyC+%(Uvf&A`pReJ|X^!^G z&_2>Iz+3M}`S{+j=}ZTqhQ)hIP=dgSSmrpNf}>-Ai+OsPm3E!4ROjW^_Xo{3;>2-VN;AT8a_>CnuGt zWV|%70YLBGODmdRoTFU4Lk|Hd3|Zh>R)0)Zqnfq3IKQ;J%l@HMV7qaJlj6RAc5f=L zEZk{=#lfI1sm0G*v98RhRHJ5QE?+4TSl_OYdpr+ym=kn&R)a_L;Rq?M~Z%0KgyGJxOy?+_O|(Iy7)fvKhIE zNvXE#saj&cLnmgeUuVne`Pt(Qpt}Jth(2t~2ZpWTn}SuQU+CqP6yiQbQp%TS&g5Nq ztxRc^I{26Ey^ne4vO4%guhJ!N?9=O5lOAldCj}rAUryOejZu!5Y^=rrje%p3i9E2^ zXsLhN@jz^*`r^|xqWcg~pPEw@y^L8z1kJ$2gAO)iyzKd$E=L)Ei7&|Fq@A#UIfuM zZWnz%EDhj^$wxh~hc8!!n0S{m^oXTru{jh=%Wt_bhc+CP(l}t5p3}@8K{zR#f=yES zULWoefc70oIozB2%PnP+UV5{YA%JwVAmFqsZE6N_ z@Pf&NSg+i;6a?)1>LLlr5(p~q(|%mwob$?MzV+ep;VI2hC`~|8!38SUZ~|D78;VP(e;sQ8(F9x8~BYnvYOrQ>(oR1 z79om`yOiyD3>_{C)ylW@G}K*E@dGpds`XMWNrpHUQiU$~@M>eA_Z~LEav3Do;b3-vTdXtVJ)G;zz-lIj$4p18fW-+? z%sfN#YJc0vzf~50837yT4YxyPio#dH092rB1ivP7;q`@)H?<__uUodYSJn-V(d|3B z3qLJ?YGLC{pC_+HPWR{S>*{UCY=>wEROe8t0CD*bMlF>f{Uw^FIfAm)E+K-gBCD)A ztS1Z1N&r|4r8P(!X!(}CzdrOR^XC5RkL~SxOF~tfCeh-oG=3)yqxVoPNAJ%w_Xvgq zt^htLiG@M@R9C;o+7VLZ=WDZC0$;5zhoSljpp)bVHRfS|dAn5LdqV(e8?Q-hPqs;E z?YY_0d4{#o$`Jnd{Tiko!43TdG^s^K%HD?!*eEQM-S_;&nM?u%# zfY=hgzfB9U+4mTEkrJ!s4wi_(eOHxl#x!y7D6FG&9+b4E83wvi|B>L;9fvtO;IIw= zR&Nu)Dsh?N*I6q&gV?7)`c7nD?k+8@wW&S_G-S%!5?3<4x{FT$8r*TZddzu2E>)~m z7Ud@JpFT`=`0&W`a-cT2vh1z|>`{}<3(_HH{0abyG5gGO6y!SjEj!$ge(SDLx3PUP z?5{jXc9yp!cFCq_OX82br*TWWM87?CRt+F~ydMx&PUh<(h?!)-hbYM+*G3bYeGY1Z zLji(7k5CFLF%7_0r4mY~NAHic{53iHHW^<^R+_X#c*>u}$kOS68dIysZXG7!D3FgQ zj_I=YjkBCI{=;5pDq^=;y8K4?0NY0UR`c3k%!420Htk33@NeT;#Eq`d5D7YbDbL1f zC&eNuDyI5)VAY>gJz*~>c*p8n)7MU-z{kTA9{Yf3@|Aw|kP(8|&#S=9$&JjJ8rjb_=s$Uc zZDS_pHo&@8SdQ>NG8Q{oCDF6V55BcOv9Jp(Z;9ivP*`yzIL0FlYqhN22IWIjf(p|) ziysnQu^%mpzgDrUGmy#V4Dh8U^IJ;*AQGkla3~hyqbbKqEZE2S-}0?#dY#AlHK%vG zHG{PP=~iF@1dQ?c;fd4bg(|@3xY!7JY?{;Pl7#ov>A4|s&fhlq?sQ?*_^+_glCn=^vgH$uV(N68deUMX+_?T%xXR zgmP`Dx$*P!5soU?aZay`kgH3v0YrbLhz-Zav6^1gjS~qhb-h(B{ezEL`Pfk%s#EDwi_3$@6!rC0$)z@Jedz1J+)V!6BDaH*5~f3MEp3 zAAX*c1d|#QLRXKuR&ds@T5`=MSWzw0KN+zhr^jnsQbxfzHh>+zc>voaAiS>HPhP+A z3_7|;Y2j|uV?CsId3;luH>jG00{CD`1$z4`V=;z#x@%HXF zqBgt3wCAyJd_Gc_&?GLd1y2{QRz1~#G$%1(HZZT(Y z9LHM?$};0>?d$PzxtV$&*b7FX7K$a-Ktds?6`Q*?4X)D8@5`2(?wfid$Y`Xaq)~0D zTmr=xdgGVz zG&`#`FY3}TG`ZIh8J0;w`#0I2S!Rs`)$OXH##0v=jJp-y9p*=yB)f9a>Y0aZNl`J( z1HE33-KymIsdl6n*G#`0&DP9a{W;=~cRwf|M*Y%Y;87FoMT_Ja=F(8}s9N-EpOIb> zTMJTI{#Ckrmy8Ju9o00U$N9IGKx#R?C*r4zb*vgF+}2^`XC7_Y(mZXxflcny+Vb~u zoB%R26A+5y6**f5BVH87+3BDeE+!pOl`^J4#{BSWFH3lSWSZQH3U!`Nx$$KG*tJ*^ zX5~1&YAb_>e~CIyyba=W{49rS=vH|rfg^2;iYGi&0$}MZ0eM4@6HGOc7kZ4#(u8j5--*l1wtHrS(b1(T=1MwAz}sw#9yB;r52|$?dWiyKOyth_ zgZMY#@|v2t=>gkr>Tsa-DTb%d`llZ}W z8uIJbP|oF*B$d#gf;#}r$@sX$eg}p0+d1T;5E${aeV?UM-|H5vrY;b=cA(zZ%I3qT zt)i-b$#I=W%=O|DCwu{k{UM+Wdw&)LKR zl`Ogu%QHI4GaD_b9Lj<<{(_4yGgqn)lN4VypXXDFdMKS7Y%OSLYof+s3+TyJejeNZ zd*0%bgKWWrQOJM{N(g>55VJkAFRIz)%6dgR!)caXhyal|{>tDOWcWM?(5qR4L*77` zChpJqbCoE4>le0}|28&0S5Z~Nq25J2$Q7_!Qlb8&PDdA&O^t4E1^pEyv=lzjLEY6c zVv#|ybI30i4g4wypq>mQdG2Si*gmmm()GHAl3-6Du6Q}+p$fl1U&I>H-w zNkONJ0miuE2MP9knB@R5wFfe8%&;|`!IY=ik+Rlzy)Sfn1khs@tCJZ?_vECJJT)bc zM;8SDza~(6-Q0yOO7WxCed>V9ijj`aI4PBO3m9C^g;Fpc_NSE_Fc7^7mOb5H)KFD~ ziTUNCY#bOTBYe}Jp8J<_Kb#wx&SHv2ZCuN8B(=D30*JvRHJQw2pOj;k*tJu{4wHb5 z)?H;YKIVzXjfFuuQxMy(Kt)crZ>#x#jGn+KBFUb>4L7W0LLj{2m$8(CYI7K-+;S%$ z@Jecp0As-e9{7Eyi`87c&1JDS`qJd%nIfylhHS3R`J%KMkU;^RV9yx_pgACyng;m@ z5Ct8S)9L|{o`$_zgGF^SDYkR0*&wbNuGK2dT89MdLmD!?g3IgtD_`YZd9tGu7Uj zuoD7ME30DE><#@ZJJ51U!y_qf*ET(P>14PtQz%qmYHj=(M>N?>*0yZP(NesKKqU2tf zjt6?SM#Db z+C-k+fI0oTQp{RYEYr3c*UA)JwM(WJ${01s6j6t#1KP6jkb8E%9K5K8#q7--*k-uU zmFyN|%klYTI|bc?@UXDT6Th)UP|p=67F9M1tP@1yxp2P06=Sc<6~SZ`1LI)y^c&h< zl!E|P(Qv(bE`DHQQ`T6Aq0rO2E3N8pRh2?C8gL3XJ+Qz94E<-3LeptDPL51;65XQukJ|D}}!2T^*VHAJ32WR4Mk4 zNVF@~156?l0Lqu8DF5`%l>5d~zJcRCng_Byy49R|4ZBO#V{|#+a5k@6Z6%=vr~Iu; zIw(yQ?sJKjsm*w9-RJP6cbG}C#t}FJ?=AT?f3S2AqIL&FKt2f>%vz{yU-f=kaVF-T z+JuBZTbs(Ob}wR(jY@`e5#f%3vNtc(_K;fR?}vd&F=|5%#NfL}RUed`iy2OLz5N?R zbKcuB*V&nKj3D>4dDlz44{WnXlJNU@0`{QGU>Fe>O z+nVLtGr&OhUp@}y#-dfxg%zhJ@1Ek=j6fm11{JLlhq_QL<54-E%zU&U7kcvQc2=+M2StJ+*$KR9W<_h{b{bM{9p4$|W!lP-@mYcm9 z0N@UJ@(VI;Zd@kr{Po&5F>8TiPfi;&4Ln!+{}_AgxG1;o4^$CQKtx4E5Co+J1eB02 zl`iQHi6MqixGku)-s=e{B2ay1^P3?BD zCe$*Xf;W6OaiyYE0h8qgcOaj0^RLASr5WhxG}FsCoil0lH#ht29HwK+GOex}a~e~7 zWPL~diyY&`s|82yaM_1ny$molA`=1o$+SOpTSR*C0L|#;;`}?aXWHrY6qTns`Iu5^ zDh(-Wo)-t%3>EfPig*;98ftjhn#}cFIwrWi^i1XydmGUWf%$CHvko#iybcAB>Ja}&RfQw9rg7LXn-zHAVRPS2SfBLxuQz71Ptk9kHCGo6y z_BsY)cL~Gj7)meYb6A?s*35FWVl;)Z$soAg%T^=fxoq{+%@2Z?5iTs+C0mITMPdYG z{MOa9R^73By~-EsZ}bRk4eOH~nw5D_sd=O?v#mFH~fO^~B}=aQm#S6l;v-2im%IyOr^!j-@51kc&GN zag49LYj))B#osPhC0ZR3lfUofxDda+hJhi8+`(Vt5V*RE?*T~G8YE^KH1fe#@{>2! zRu#n1F`l^F#s6G|`Y@acneiJ1&2Z)h?hc)=4mHDpY&Mrp;>j*0HKTIM=Of?v@qxUu z&JDN2x*|{un9OA`>aA+tZe_AR^7^pwT{kMHL{$a@ScEk9vKs5xE&nI{ zA`lfU0ofq`APf*wB>UVNN2N+EFCNypNF$#q*|jILe0A`N&br?Ab*tcE8K@qLW}obz zMTG|3J@{59FAWymtxyP-c)L10l*%)fDMWZZL+9C`YLPLTJzFm?e$=IzvX3@fDtR-J z+x+(EXF$T)K`Ga9d2$_WaTN#oRq*;D{?!_0 zDi6!6>-bXo9!I5#USGr-dUcr9j#)^-s_RG;y-EkD3E zkNdD#7Moh%u;7VAmM|}i(Wj;e9-aROk+p?$?euJTMfY6!RduIqmv{$F5ea*`5AMNf zoWf4y0%%l$em)Z#?UHqV$*GNT!;5=@mXMsYMHS(xT&yPNvV+btndU;ac4!ux%&%Qa z>%WDtc{~xn^Yl|2i6O30>nBoOT2j5IZ*^HdEKi(p4wt%>AFjrkP7mMfPM{uQ01DS< z-)uIuDiOQ)*T)Bc)iTG@*X`JP6L9s2G51)Hu6)!r0q{Z|04)I)xdg5VImnCQ)w&Z_ zRHv%}pVm?^%7!4>mvR@|9$OSPZt%U~A{Cbm>@_MXqIAw-o%?X-sS>o{&gfE%m^K;C zEIU^xKX&E)t=FDA{~+kh0D`V!I=>$%RNZDg4fU@N^;X{|9|URZS^C0(zL6U)O_%!8 zC3YVuWH-E@YZjnktJ}x{9w`cl$k&dU8K0%t4Zl6+wP`xX&)n_!t?;c62rMMPiRghp^d5>q`vZvmsFHf7>Ww{h-(m++$&S@uPx-eLrrSoiW2G;SV z%W_9Xs@~Jg!bS#Nl8AA=6`{sIePCKz6O@LRgBef~t<)#mJV0GsFWQ>_wveF#$$s%; zEmA6*S)IP_#Q(G_ZBLu0j}Iq`!0AqSbq{;jZkNZPeQh~V=iK}N)H!z|Ln|F-(Y2eg z9y)cO)0^#~ROLHF`|PA>Ey;|btCC(j8o9oz*2&6gln%LLmzOil?+6O&f=eS)_Q8%+ z0bugo|BugGgXW!4F)i#$3Hs%ps990!N%j~W?`teJ7cEUQzsyrzhUxIm2 z#UZ40-T6~X0ZF9R*UfozS!wD&UWyh;7Sf@=d2ta7B%i|6ShOFFbpim=o7!7)fYf~1 zBg{}jgeu%3wcS&LoL?}V;rqE==6%F5GCs2RBjx7L`c(YDdAN{Y`S@=C&G3s)Wm4UY z3g%6Nenr@RYTk80x*Mj`FJuZ?xu79KwqPxaQJ3M!Ui<-+y@)oK-PqQ~%OOW>!Xv{WYXUX-n)uYBj;U zuaaBN!?|;VlV?=KeWG?lP9cGqmx%^-U24n`$LM(U7nzL3@4GKSBBG1=fP;Oe@ud*& zJX@dA<;-z)d5!znOfFN#8Zpkj-B*95=?xzOLvPlxvmt)^&N^urWtSi2uwOcB{w#M> z?W)hOlC!M~kTI5-h}nn$-#5*^?tfHC4JEwm;&dxXJ?pwlI_&XTzXR>A+`7ou#(eRd z22DMuzW$CI6dJsDbO%fC9HSoqy81k%pWNJQTHN+;j@W*IxN&KADb(&t>~W$~ty8qo zchDJidHqEW!)B;wHE>*0*r7D0)v%@<7rXViQP%BUO1ckr-K{Ta7}H3wk~5h^PQg_w zcz*_Z-~aB%A5G!jxU4yJO|9SP$coI_L<==4x!;e+WW+zCUyGtXircx1upv46o;iM( z3L+$(%Juy`ZJ;ZM7xOp5UGy}e!9XrK198lr@fQe0;QvmS4jQ8mR_Brj+C zvfS*)?;Y3%Y?ALHt>4OfwQx7UPx;)!Hxn_M?#1gXOO}8gE=SNY`rIlLS>L$R!`n-fRTdPZJmTEGX<}>jqoX3P z15_>RnDkXDCp-*pyPoTGZPh$E3*o=av&eg=37^`V&xcW|AIxNN2_bNqjdlw%s&b(o zNdruJ`(-=Q>Ews)*%=<@KN$_>m;-@!aoxFt+@K?7@-o}M$l0G`G@0X#%O_58gH15O z#dx;TyF+g^hS2TWrCBD|K(29kw1g|yZreJGKwl**)Z^38@>~8N4_?8H_~W#Bd}GK0 z+b4JC+kAo+e<~|pybgf>J?eF3dT~{~Yy|glz@XKAd;Rj$XF{egdQAyD>X@Os)k-75 zy&doH#koxRV-x2Obv%)5z~5%(vXK9^sr-ChFxN}~Q{<+pQy@3ndjzg_5f`;gIgat( zj`xzhDQ;|$tA6Q3X>OC0-~J8sKs`+o;C)mgNPn%hwpuVLVpiTZPeqA%7x-2*{jD}j z!G4-BbVxKj$vmzIq7JhhqAWzM5Ff{OLEx2IdM+@7frN%vzmmNn?pa zL=v2J@7)yy7dVKt&fUV;Qkx>#w$}0se$#KRw&jzqe4xx3u-I8&`Wn3ZZ1#>?qhU`Dp0Qi1}_@k?i4*x zsTgTla8<+EJP9pVt5sFlEmBBbU5+&8j!$}0Q%+#vlKWJ?JJH6j)F_E}kzCyi_>W*8k+_L(P8$YTRcg&(Y zaZmH8cN5~E-9F@^wztGO>;h9k)AzrD+nJ`Q@ngAxL`urf$E(m@Kz_8yhcTQ>MRjp{2atJ^WISgz^lJc|DD@ z3;AK;1kVno3-3!V@+UE+gWo{2-LkFZ)JAyvZi~L zqRW-;{ilw{irgq^$R^Fh0f#FWmS_-YKJr!B|(_`_zx$)4QdDAaTM zQ-VnBwn;#dtLWCBR?^`AmVV#PY}T?rKuvolktjP4cB8yx=D9CbP=*Y!x&1ga>d%=` z%VJ?Se7pN2V*YKyh%Dh3;(}>NZ_rsht28y-xzJJoKqysnZykO;O|2s0Yam37{p&dV zeYU;Fm|165eI3MM*O9H9jgfczbr(8~eId#lw?X;~zp&o|5U2fXibg&;?Uc@Gat-!D zmk4|GNP4T9&}XA=R{fa`*7i@*MZLr6x+)<5KJF#|+bjS4{u$-(>xYX&nY8&S8_URo z^})(^V)V~PQw2am04^{lmyd&qF@GJK;3uCAjQ(?`8`)}*Q}zrT!o7O))GX88pr1Rc z$Rn&n@ynNpU;gJI%x9!V0_I_@g=UU$Y5RwH(2O+HLWb^WKse{5Jd_+Yd|lIMwV zv3%y4B_8*;Q@p-(o1{jvq=8Ma|C59`DG#$NTlSw`#J|504&GW*Ep?J1MSDlb9r5Ll zxvrnab_iBQ_gH&RN=t6(PVUm=Ud>vDCz z`^tFZ!*wc@I8UinIa)D4C80{$h8AwF);!NxV&mR(7XtR1r6VYy*i!;U=BHXv{3CP! zYu5QZ#BlNHoR1#7Pw=h33}?KEC;!3zzz{Z!lFy+fgvJsn3rPtZp>|BAHK<6(@& zi2Z==3b%u5CZz(4uDvd28o9%juGzTmsIVX6z#^@9Ac-fpHCSSJ%Ac!%;%f{@81(k) zlbV7Fl^SeMQ`Ey`GWcB zHmAI@ZNZHR4(9ftsS7PRl-LbzyJM`v>gB)ybhrju?(%MFJO;PTKYC)W{0;y+tE*MY z3SYWSj0Gepfcz*yHqad;Ib|1oig^LY?38TXw6Vh54*=$zc3;%xzZ(sBm)kgC6=r%b zeP+apBiErhH~Y#8URGgdCvqdv_OAM_eP_Nn*Hng=pEGZ15)wW^ci9GFUs+@GeAM8% zfml#PEf2>-CVoT=NB6g%xA{ny(}Zf~Yq@|b(SRnRN!q`=^|dtlsL#le`c4VO>R$_d zvIoPDZPlj={7``#n=%w5hU|DcB z?(9kvD})soV#_aiV$FXK-$hEW$TRvB^}Fg-ORUTVt0=7^PH8-&&Hhtj)7uS8GuNvM z>4{@KT!SVe8PqTNAdXW@L^9XMz=|;Ewo__%4fDViglQPl*^RIUxRK28wSTR7+YJ_) zHwtSf(vSQDQ+9?Gn%r>Y{=c!$uZ_x3;rq2HLYCXlD^BRZpRYVg1}Rnio~97o6S){+ zM`i_2w&69K04p{3O7s1SDvDz!$%U-1)2w6V|Uai{bx5r#n6HUrv!%Wz^ z+Ro`2EHgc`l}ULqd9XWoU_6y9%bQiKUPK(E7 zlsylY)d7nwx{*j&Cs_E{84VVL4?Ea3B6#f}?bR-uCjBsD^RddHtrd^zt~lzcljCZaBD3CG<|FVp z3Ww>izT&y}eQ?cwoleW)YO&7(C)M`zJ$kGa)0g9fj!Z$JGWw2QwMLFQrsnMw-OgI0 z0p(TCH%sKnKMvN^X31b&0QXP{>j83L1rAChjmRpEI4#U&IVl#T`0> z?bh1kW%{SzM%gL~28CAZ^Vb`oh)GnX?w9uAnc-KYL)9+Dfz`7X!?jjb1G(zzBV|_1 zc5_`(?~R(usaqFzBzu;H=#rvizcu{0SH-H+zX zrQ^A!5>F;t*^6CguQF77CNA8n;V_l#4veN_L9LUoxx(*LyUpSmc2nJNipDs)b7ceG zhee;+7~+I1_%d~?t1Ymep({*7BgtWPu9S{_VWzA^OC<`b`DNrhl}9_Myh$WbEX-3> z*lGN_w>j7Dv3sI@A%m*LuCwlBy=v4-+|FTPK=b%u$cTPkg-v5NuY98Hxu(Pomlqg+ z;5MrJmLEt?UFY`H89EhF2YK4)y3=X=%Gm`Axr-6lM24}f$k zL3iElWmI|l;B-pBailS^z1!wC(lg<6(=RQ6%!kdvi1%DjL<=yHX9OUVPz9j_71p|? zA{~d1HH=I9knT8|R>+IQ;2h|9n#%|JmcN1q%;}f<0B`#I*t^Fu%g3J}kV*li7n3mu z-1k0rr8dwcwe{@v_3E756@_I1|KF^#ZahQ>>S7CtMiE=4`7#f4A%{nl{>srxYM`^Q zZkIcU+^qFED#;=+SuEBT$6SQSHmoakHWVvI_qIZ))0y#S!IMS#1gxuX0My0m@C`<3q@~r|ZyOrMBlgiQK!P6B7DER%v^uAY4?YhWe z`6ozTYeo5z-1s-wlPUU$ta9tt;fWspM=;a`3v*>yu>y&M*ZvlIqnwP<;lU~~TqK0@ zP$`6_bIT_^zfO@;64pv);C>8*6zY9^**9`>ZE@;KMlm9oKt3j#d%{(DLJOS{u`k*9 zim+4V4Si3kby5=LXfP#z*9eu6rN&*hz9c}q(QL>$%>M$rF40=+k-f3QZ!>3Z>$Tun zaj;V5+rY-slcUO9vEk1QFCrji?NDaWc_lwQRQSZO7RlXJTl=N4QI+9uy$H;010N)7 z;UC9rSwFz>=PPx)FT6`!+y>!?_M$DCPuo#i@b*R>de&=?mXP(RSytg)dX^(B&Y{uP zP;GDETuKdtpt6ug=#xBtm|O=p)a$64&na}CbknnU7H&`oP|9|T?^yY{90SSw1qfqx zH>4^_ZlfyMvJyR(<_39vW?&As;)%n{yyZB$S@}21KEl-@SseT`*hI_K#hte~_&tgT zNC7^z%jkMseV<7?{5e1!p3{DNf89NTZ{DINHYTMQOA1&-U<@&pdN7`2tB=kz_J9j#DWqTKxYNK$Q}TbYd9 zm|rdVsl%B6OX`x zKjusa6^k3ut+wuQsCLWxy;}@_DX(i*t~%tfaJ!IyAJW$BHg z@>kpOh&$#ul^+za^*J*Bk6ye25Keu%bl+w!TjE$d9vsW=Iq1rtl4R`o#xiUT2|<}* zJg<91tPI7{6G;&v`QZ0t#h%7&lCOR_IX)>xLXECgw&pFdA zTq1rxG6!9MIrgQd$+t@a z{ga7t54BHiQIrJrFF7-5Dih5Q%HWE+T+o3~KxO13!rA3IX`u-Y1qimYoAxJL{qixO zK-fm4oYh8Vn)n3ETxii_nQvC(r2n~z!`yQM)<`eo5Nf*{uN7JbH;3BvY;+3eUl=TA&`Onqj*MZnpEwYm3 zZgOmEIEN(wQStSpNqHe6g$JLAVpKRuJQ;WumdL;)RnFeNR?4N&KtsO3?rr|%$=K-^cxOy=2HND95{0_&WH+sGz2k)7! zdv0P&qFqhl$FBDEYAi}V`$EJiRJBPlKo#@YoQ{a7=H2D=X3;7sX`|yZX9874=n&tY z$py;$1NhEC*M-ONz2#2!OnbdY=t%V`C@w*egAS#)q%c8Tl-CIhSdYOJz-9o<=#uMl zalrgg^Yq<=eb4=M1}IZY0Bfb>gEMb>OBB9iLjUfF0Pm@Lb}kheLmz@aK0NcjSHaas zZkCVXL@6Hjehg!eq@ z3F1sZ_i^h70lJGijdyadoUrAHWXV(JF>#o7;cGj@#jW(*ST`Xx8&0cG$%&-)x^wt& zCu^m=m}aC5itK`KI=r}}Rnh{dDSPla_~=13eOk_ezQ{(5Z5KRip_patWegZxd?aZ}UHAK`lZP2i4VE;D<*#Ks zV>RDE$>hW{UJyQ6;?S(U0~NemB}Qm1xVcmeG1l+$KpIs#B1vNOZ}K~>L}nfDMjFSO zpMThYxtJB%gh%|aIB$1HOT*~10K?`STWBrAc2v`=H&5Ed!-tDa6`=kN&+|o5yq&TN z8lwX}vdiyI^AqA9AMNH8cJdb3G(5+#RZ6Mtd-p88(4_UzDIW?SSq^r6eYz;=%9`*6 z(&fJsgV7>g5qgiLHBzT<6h=O6He<#25$F0kovh6KMhNC*)+Rb!6moDK$H+%@DNMuh ztvsP%kv+7F!au50A=gJ&=2#+H{+V-5oU5Yp0WR<9`?_{k`#Fh|v14pyV#H>>X)guV zCVg$^=6h|oOOY+Zrb#RyI*Q3-WMntl0QVVK5ZQZ&jE*Gk?!e?W^8>JiLwW0tnI_y1 z^6qFBsqF6vO2u=U-(p0SwXzdS9_-Db0RZ7mqDR-Ti@0u*^GCdJW0-a#w^!}pUj6l>tiR8ZH90=zAIkw63?r6_v63|y=q{r~-KQ)cK>)&3vy_ePMw_#y8KYF@Y zNa-0>epAnk%6z`P^j6E6EcNh8)#BtXVeZ1B(cLjoO$e-+CMvOhwkEE~(G*q!Or0D( zL!7e>Js)(AT?~Cif666WGdK5{oWzTa7xFcR+CbqBk*O%ZCXqf-*B9Q zi58tcgWCcvsG(2da|pK{tDHez*#lT3mQ-QVL(jVTvJW)nOZ%0B_kW?H{>>C#O!!{? zW8-@nh1(!$qCNQnp+`1W4z6_RF7y}EX;gwO7s432^Y5G-t*0Iq#h=xubMbQ+X*}qV z!FZttdqwrB)_`6!d)9*{bSxQ-tV4CaBQ&NghC!bjfW)aOj{~-SFoeJ zbg2r(NzpZktw!mTCEm%dilVN^&rbA*1ztGf*4>JFr&O8)P)YQCU-P5;&(3ygb2@EP zjC{at9eIhjlNpPq$pZIL5aMu%ep1$5JINQQAdVXF`&T*}M{HkLT z@MF!;J`z-rC|=Nd`9G3Bn2OmWs^CZaBA+r@do1r^AG;W;h03Hjpx>o;Q;6o9dUl0Q zkHy)VbYoX63}KrqRZVBj6jynE%0lm051`J{gG89MsENZ^@o_;W$q-^l*dwjrdEa~i z(G`0QwbgSiEW|{T)p22k4uT;|ccIVFt~!ow1w=U8WBCaaJCTlh%stu7+p*MA?xGV>aR0PPSg6R$bGY-AzM_gk22l14r z`ptPbGCrvY&z){~iiW$s%3K6GMyI zaVufns~kjX9T%@E#a$VSi)P&QaC#h_IZbu0ce_q$hcfi6LVAdv*Khy13G%<43 zt+#R~?==)8N}+D$KFof|w)Qfvwg74ra%kEn+RJ!?FGEgUWqx?NVs1dX=&AB?L(#{P zFW=r0Ut73#lnD()^BL1(8>*(tkMV&sQ0V=L;J&IT4*m(M==sN|!+(Aw^;1bqnWt%W z-L+|zf%@y^0%&I~JAZnM6*Sc{67P9fTe67XqY#SZNDd`s39ohP@})5e*MA)H{_V2w zKfS_+H`-y5jTx7-n zm%pkH*oD6rlc^16)&8$K;BTKC3G@^1#%sV8JT`ztg4Z>35kwF7h# zb9y;z_Wgjss9tS}@*ipJ+v=bar<8BaK>ABHjIi)Lj_E+Wb6c!GibLTvpxHrk~D3V{p z#eI<3osn?HgdIrRX(v;Iz~C1c={S)l-=BU# za+r6#*Mq*`(8&f9gili8vVO+|{nk@H>=^t@WYcSdsByG#&u5C zN1k=6^cT-b!d&)_Ze+)YqVglX0%wGHSN|&D{P<8G!*dt!G2-fpswNSo5HL0W3(on~ zt-npAQ$FEQBWKbaWbUc>WXy{O^lQ`h!`nt|7**Ywc5|je--psA(6yib)(;g6rc=Z z!3QF(uNl?r{2wBkzmHt%DNyginY=Rh7)%2=kLSR@9aKmYiwb~#l->;?A z`3!yI+o2E#R^YcZnd*G*gGbGc{`SGB7#3C~7yN&rTfepM$J7MXvqxq+McY8jRqAh< z&fmMx=Q)5is}(8?NC@hMd^_@ax2Sx{=dlkN2c%6O_$X}5w3}qRnBZddmC0QP)uB@R zr6JM%6vz0c=1mE{0hTR~h2tZ3*R3SAo%!B!peoh*FktP6UhV&}uu=mF)=H`aCqyds zfoD%&-}GCF8)4bXSu+(w%lwWl1$J{5oEq};W7_eYa^b#bFSIRiL5+bQAN!17Hpc6qVs`(#rDaUNykKeC#b-(XW2J)i zG7V0UXuk|&_SYU!fkt9BGcQT?%bx3(S|B2k`fjRPoZW)D`o)mW=_X?N&;Zuz$3n-` z#4d|TP8n{y)+9+mi+>hJ{+SE!CNWo*{Zr00>kz`bbuxMVmSPuqobNt_$;KJ#E&w(0 zB!+J@oNLYkwheo8?e%uuz*{DlpXmMn_81p=NxY`=ftfXv-~u*Qrais9vL4QTt8v=n zrEbNHMb}G*Oa&>p`2_#R$bEmS)Z18rrFPrhw$apLv~k&nThM-X}t=xSaQ`0!kJWa zBoE4#r(@GY!ig0G?kVUV9d;)P>(?Ib@N3pRsmWMDF%S}8moB#+n;y063daaOWv7#| z)ovAGJTyS8OpUn-?roB`2=sSRc^%#~Kiik7+O7uiDE91I95QM|lyj)ZIrM%Gy3WR| zmXG1WOO`iQwJJ*7xY@k3U_#3HvP!-cH7|d4&#@XiPo@EU5^y0k)-!bd| znP(#A7=#3&NtX|Jo}4yExjcAjq(Vk$Z%G;5o9=cc&scT1$Rt3=W7(m|WKoTo=DlcI zhNay5=PDyRg*O|gKLZ?ubkqi}BcvmiF+}M213+!CAoob`eA5*=I<(_0o4dDBz`q(P z9z>24BD_Cw()Qv_#Pd}_h$7gUI)MV+xW|d0%kl7BhD=Q3^XBEGR%OqNlzWaHy%O!C zz-wTSRKJKw6IFMtbq7PZ(0Mw(XWReox-Dh#glvkC?#dx@?DAcr`&{UXm^-?V^TH8R zvXe{6ZpS91oyCLo$o7I-`iRbfh%QFi(jY)Eic1|*!Q=y~w?1wv73kIIY$th~e2T3( zo=s?&0wtKT9l*1u)t8SoTi)fLxq=>g|FBSYzN?DLomx3dJ|bd2QF4v>ir?;!{s!VX zR|vgWgvEIKjDd$Idm(tn9`v9VR~XFujH|_Aky6Ogc67|e;I9=y%F^1&z*cpeo2%Y$*xUCKo*>x$+3F6d9B8FGt= z2G_2n_mC|FCffNZpK=k52wT^2>gtx6TE zK4;;R$o1y!@UYJXa;W=7Hmf)Gw#zFv0`aN^w|W{Y7I-F_T)rBE95}q!f;7=9*`AvQ z0bn)s`=R!&;dC2AQzub_P(+&y;2}FrIK&iNJk^(|IbyeiZ7ugp$n)+QqX#M?fa%2$ zpXUoaLy%HM2yh;0e-5T%a03aA>=JV0bRQ<0#>dGAP zONp3Po5n+)NA!lcqj-ssY{kwV?KT1snJ_DXSn@Hyy2$^Od72R5PN z=Q2ADq7d5n;8Xoukz@{ts@?USo6y|3s(S3xT<5sVCQp%h94t3XjZD~`y$}x0Rmqd% ztX8145;OO9_EpgJnU4W%?_pvC{;RQyT5r<}ud*KYlPAH5Bp&utdEJSG=GW;}L$p4o zz~nm1SIZ|{1J08P@RU?;dw$K3+E_~ukpeVRNo8OWw_xn6kl6Z(sg~= z#r{zXLy8Bhoo5oWOblDh3Z$}w;YPdg5c#spJ3kRIlBdB7L7xqL;ylY>dsRgs4}#z< z+y8`{WT>VuCbiF@tiNw6L_-5?gGL#FuS{ueqEe(IKJ()FHgxw_;F*o%>o{U6nD3>4 zzQ1KsZZ(Hm9sVQV+LZ6T$E~B@U*+wxSL|lKy7*A&UyDDF)zR|y9~OUeo*x8qp9;-1 zQreZ7Qn5)V>oyOQ^0STJ@3U1KOFPOH9!0khFi$!KzQ)t`mc_nxvByunQf&ZOh%lY$ z5=6#dRz#NcQFk`eYizR>=MSJkt{OSPBkg&w;dxKS43K7S7uf0#)BmE>sl_FC*E}^N z3uMephfRQEj0sNN)@f7__xQz~n{RizFL;{QU?gD%yM-3vg#g;<+^I^FsWq4rUc~Y# zUZ^eS;znIE1iz_gGUjUU=Jtxmb5+-=&(k`koR71B?Dq@jisN`Fyr=>{mb)?-S@`zh z!(zLdRmAes#kYp}9ZTGx+ulFd_uO1l&XPGFQyHI4xk=z@zIS|dkbo|?kkZnsbhi=@ zcuDV3$Tto@$~12(eDcZt1~7@fWz^QFO&N-sypQ~L=1v_x7`;v zZiY&vf(;*1S*~Ms7zj}oim!BVPgvG12lv_{#~{nh#3Jt_aNl!o z&ySDAZ?tw1jX5s$jJJ&xK6$v-12#3dhoD5jY^U!Y&hk9DZoGWQ1*MnFxr^}*%YC2t zwvb=p8*gVSW5cZWGMkpnVP)21;khj!B!=(uEx?~m!5Pomk+gWGrw^yek)MY@!Fm>F&CVF0uLzdbuS2JBaiBm(|-@^wv;VPj9Kh*9z zl<*?k9d5QgsW}P1>5JRs*^Iu4MItF8;cng|?W)0-BNGHY&V7mXfsg8tlG>W#Y~9rL+gZ*-p5kp@kllb8L#^O| zA!!vf*@xcDuiU_29Ex6|?)_MIwUsVLH`!*+fioNy1o0Q)M@wFnlM9a@(!{ZLVEVLu z9ieRkpeZC8_p_zc639;xaBJl#Ha590GSJd$G`kDAaXwY+&6Fv|GLS1mbrtx~zub~L z^V%ym%&u5=?QwyGgllG2RsvGAb;h0jXr^TaS&)jm|b2 zuKJ@feDwcZ%|ZNRx6F|`@sXv^J~Ds%#jEymZ`^k)d&oNYSyy5P54wojeWq)t_?_=I zR(>1df&!6RoU=us7;FS%vAo_?g!IZWMotcv`Vmg-yF7R{EwuRBwz^$GPUPTelKU6T)+= zTheEh-b>%$RELnl6Vww-A-E9uuD0eUSeL;BD7cShm|r`$wKB-~E@j^!fHELLY{sj_ zl08>kdd=?_d|0|1cMn>oSzrz0YRiwqYrBCe&f4OAUp}O)eRall_HaMpkmtx$v(D4k zl6_Yd#j{k{Wjp+dR4Q*No5^W;gf2vIJra+Jz6Bh6NvpmKHnj(Oo`+~tw3mf2+DUri zLDl>Ez|jL>d!NTB>A;zWT;zamj8VazI$M}Ak=4erA5}XDtAGn*jaH6YuElYnr_LkW}L-;REYnWc62oLI&ySVu~{To1}>5NgdvW7Me}$x&dc>iyT$hhZFvf zvTj59y4wBfDJ0JQiY9!Ovi+~0GzOsDI|e39g@7AoEYqgf$r%2%u|u!&I7MqaIq#h4 z4*fMhfy1FG+uls|_-uN+0<>BW&Jvd>B7n6@DgXMm0@>~iHnfwI(DSJF>D%g~l>_$a zYJx{+^y3!>vT0CdRx#h@U!T5&s-IX^f7^hvn=Gi9+pI{@nio{$1Hqt{wz`ut{{wfS z#hO_(^yHY6SwE|PU=e9cQ&1y{(T3e58)49vP2Kl17jC;fwcg8+B5o_)1};(eu%&@O zuo?HLCI6PrcZ$o;RLPGkvrh{qxn-@I5e`42Oq(CeR!uXNDo}16wLM~v@L>wd5Y0+B z-t*dMrXnB^OpiE5z>y?nPA~x$`9I?085S2n!*uu&H?g;vLSwSX( zNcgzP^OFSJ^oBPqKIw5|y#3 zskLp|TGh^Ps@He;H`)S>rUMj&B4bj#)abo96vg!bFv1c=4}fS@8IS`~$7QZg$wn9? zuUz|tR~qJV66C)B#cgAQpu2TowKiIYRX`t9W^NYLZOa$NZAuv9ToJy`&l)>78prDv zybb8Ho5)k7%A1awe=o@5SFgouN=&w=-p$vu-yX=8(ZYxy^56*aWu`_I~ zSk)x)n1`EDcgD?jrm)$>oeCz8QCXO%s~gBqB1v^$*_+FQET?KnCDEvj44XW6D<2hT z3xxCE8lt1*dNwww-iimouP0$XiB9RTe-#Eh5!wjVaXxnWsW+ReK)&)#HlW zLn;#y@{!_%#53g;b|%Y}4h`;ZbDL~MSVoSi9bIg#%U|eu1QuQ`cBlkuau6-f&5ss$ z7r}-{tDI5}Yb;cyl7q|svu1u}dfu%8mqE2J!dR!S(5}sm3vR5>8uOYxm#558E%#@g zNMT~L(O{nAvcBbqgsVQ@Hl*aATe34=P4<9n4N9;d#puIh?!Qx}SePeG;8ozO;n5`f1;L7R0a5B5Xj?^1mA-HCY-u*BS(;OK5CHcI9eEmY{xl80nOG01}q3k0WfOY~TpxVlGChO4turd#N! z1fZt*;>;1QNeD*@9Q81qtUwmng zT0G-2#=fz9%r)2jTd%|{#q~x6n+0S>I?A}7cdo69{#15)-R0}F{`n`WHU z*5SMmkGH$`twM|431>1Y33bpK4jl0&)2ixip$)#W-qUCH77N6Ry09URt0Pu|iO7fr z-{asUzP2YxeC?|0I;>o>kC}D$>%7MAWC1KhvfCG&UarEWQ^E3ehfSV)cOwmP$$Y6pznDTQFBGq*!*x=mZdhAAh~d-g^l z?D}Z6N$$KWgs}69Jvl{tL2$DDC~3+Juans;+`z3U{k~IuLEJiQNF70t4-Jv;9x9Vd zn5;C@ve^mL-^jI=YyURiNTe+fMdoqS%;d-%xo-Q;2%m5sx|)S;2@btBnzJF!=+8)R zz)g0`pOV}g$y4Gw>DG*DQ-ueNL4_E?GB`O{(C-p#!uCM+1ScbS(E-kBgOJUM?R~9h zcLEZQ@mX?le?;2LYwE;_uL`N)p!U#m$x+CtQGSQc%cV70?SPa zmtuQq5ibM9gZTc4=6?M8i0$H7MD={P`V$LR+380TB%y6E@8GqZ>GhU9DY;B&P<48a ze2%vM!Y2leewwb~(qa`hC0~W4j&}uS5xA+GPhkaV^n{<$%zY}E(j?}CMPeDBWJ(hA zpCHm)Hs7ICvijz$YRX2?^gQX%vM6NaQf&C$E%FF%tD+*lI5ZcH6aX)Rx`M|+;%y?~ zZ>u^0(AiSxH0~yB$uUPr1;MEoqT7?aEku^*^disZ03I zZPrMms<{`nKrZ77qADJ8rM0wE>lLBru!F-<0W_gJoP1r6KY#^G?6L+h_~=Lyp2*>_ zoG-jJ$O3(-+~Mlb9$jz_wX6^6DA`Ez(RDLUvtwba+UOphQQ%s%7*;+v(yUgXNpp6S zEGir7L^+&Zb?4A7Y%r$#b1(# z2-0L{AJ}al6?~GJj_9r{gneD|&?%UFPY>z5D_1->4ZmqhWjT=_RLZ~IoU(KWnycI$ z?DAzs$K|cfQfck+ZigLaf91`z(KjFcplEh)oW@&(_e#&Dw0)**mA>UDl{&K@GG4!q zQcQ8J2s;+5w^ZRltDwQNya3KbiOJDdVqO>Zkg_kGl>^{sH(bbR&wxGIUa)kP!OMnH z(B|8kyL42_Qoinm2vEdneHo*7Giz5^l{}8?3pn(wVYG>~p2Qzq55xR2LA|1Z0I^-E z=e1}R$G$C5v6KVN0QZsBXU#iTtmaBQkEWM*g+i&k;#tdsYi0K1sH3}Ai$`+w>uSxb z-r|*H;-YL<<_BK69+tak(yVq-Db`Ai^A;p|U^5kYbF5_^;N(e=y-_xE3mbU*Q-I^Q z6%m<_fj(59@%bvO^DG;R;r8C6e%uV-<=R8pD*N-!6r@|qUj7$R0^9eOsE zS6a@bDjiA_vINo_iOdYiPu*FcUuB7J_dTgq2Le`7(6$FjWrgMuwEqudUmg$j{{3G{ zilhyW`3{H zy`NjU_x`^Bc+Awyyx;HFa$e^=&+|Nw$d;~_?af2Ji;|_Z_krWlhYBtG!rIgHkgr`n z%-1$3!Um591VWzYh~cZX!E*@j}35 zHP++3%IZtj;TWKq&QGTRgK{uVr0Syx_hpUdLXdVWyqhfD1DnAv9xn31H&@x@p`JV5 z`{U;YyjP`4VC$mOyS(&CKhNIbq9W4L)_A%o5HamaiF*PDHrsD?PV4`t3FJ5{`1IRr zehJ~Nz6LK2s0L^65~h~{kLx9Q7N+krja6#tY&Bm^+kRj#6xc&};NqCm^(!yxl&?jS zmk3vA?V63Qal1kBYa+wbr%zwrp|0yo61-0Uu!+-_DQ4Q?w!S0cWEFKm<mw>1oo24Vo0cF0(>NELS z;sL~p=K7v}6NVK*@24t0Xq-4~&6|4>%^RX&nndRreSOYyPSZ|QOOcw{Aw`(wo{C6_ z5+@D1MgtLV9T3F9GRr@%?o;`?cc)ENP*UenKU~C5`*`Cb+zkFUdi%qQG|!KJ;0Zo1 zQS@xF(iL(tes0Fso1l>A>5k#aZ$qBdBE{EEj}A?{lof{=uT3)f1P2uzj< z{WROHv^vvf=%9c62aREZ*P}l-z;gnzNy>%iD=XM_+Vu=u7ZoZ(6 z8#0wMU?2~7y*HC^Ob%ky1R9#0?OknB04H^3XyFce8g6Z^m=WN@;1_ zd9n*B`@ef6hcJi!*-&`XVE_46S;O+RCum82$9wBibLp(!Kgehhq~Vqy)A(W3lNP5h-+|{C3;_ zjnhJ>Af2qq2mFaU9qAkG7=Pvc{2ag2WhGN0Q~$XF_pkd^xkfJzpG1GEF6}t=n+DKN zKL@UJ5D0y22sC&p4B9z>Ge|vR2bTVgc6O82ITE{xU+P~M{O9}4qr<(AA}?)P zJevYuK$ZhzH8yIn{fmI%Uu3^uZQoyO?+2?%>vU)hd#d<<605du{~rhG%-ORnC*R-x zi89E{X@1xLLx|93(Fv7IdV}Z}`@OV4U&|x58ya?hU3$|^?-c?w{Px%bHKT!1f2o@y z^}op}{5&KACsiWSk4y46Sl!*hJ=gP}+^M}nVsvQ%fkw~Gm;7}LUR9&3qrRWJ z6G{L2wEsHe$jHco^5=d3`=>wuQ}~IB4`XT5P3#vHzmt14cq+GR%6NJQ`C!EavZFL} z5``#S$1mwd|JOd$cA*naWE)JxJXa;ZR+&cPb`+;kw9U`+@Ye_ao;4wDpzq?Z(8{-aJ435} z`icLtHhwA{^o^b_^6t*v6Hq^P(N8xvPv_6~RK)|<%~QtV}zG5H~tb=t72%U9Y=|a#XY($}Qc^eNMZt1j@C1 zZZpqEEuBhN&+LlpZOgXOSS3}Gb`W@EJhV?}C5W~osD@HbL&u&zWgWw(hQ0)CX|IC( z`B;hPqGf%=$+kCFUYdB#3(zSO9h9~4SSQR2Syy=Ebz>=wc%$MpknnW*u&WY@j^JATwWAPd$3jbhow5A> zRUuG$9nUW!!@)NfsSok&eLdSE-Z69yvO=dvJ!a*qBREyuw#M~Rhi6La%9aS@DJ(x#V zIrahp&d}wyV$6cZVA=h`J+c{>cZ=n;b36tq_fo)ZGpJ@_Pd|#(MTgn=meKvj$uyvE+5tiB5_-jcbU>(S^#HeY7-ya@BNJ_^FM2mbOekU zN-qWZwja-XbzC_x=z(^6@m#JY&!uDK% z4Og(>?KUSK$wLTw#4+2GTp3w>`{Uyl)!!2{|GU_c+$}ms9$TG+r+0HXb#1XYXfQ8e zn=HdX$C&8hYD1L70oEfkLzTvRxIdplxm~&5?<6@R4fPz4KK%NI<+rx<##l=d;1?vX zcd-=`B%z1lzWHLk{w0qPVlOqs%Ll|0tbq^jIPNg;?sW6%Ge4W5Ukv@;ouHUrH16uI zF-GQ{!VxNq=loTY8348Hh!$neR`%74qq{)cww~MZX;uw-H;e7q7nRQIq0GdvwEVYK z?js9_>J5rrI?XEsnAmoZKF)A|#;v>$IHWiKBZYkZgD&d?(59}rR@fu8lJI}ohE{j) za->S>?h{Jz+Yt@aQXa6cmwl~R9N!NTPVv(x*=C}Ysbk?6wwb&4wjj}w9Wqzkj`v(T zTaXKKmC0%k=DJZ3z5&>5kzH?_S3kzrd$l=$P3A8qsGrCN`w%GSg96Vm{G8pYZ!14G zydUs0v#l_<+HSg4UjK)nCLMSjJrxo}X8$|8)|dO83M3?Tv%53JyG_pCbQ8;KDRMMj zsk&uTx;_L`$_;1V5=&g_b{Tmp_*e|>TB`{pUfg>ukVaOFRZ*gb`#V)XzYTf$bYnB9 zC^7s8JMY#aEEk=j!%_n67Ee6OTV^k>4a0j?#ccNKWQYt2s>6>KdINm!hKeL|8gI?B zQ?EJi={P7%C~l9IyblqDEMq!+^+-N# z_MjbN@i`!&eG`biXMv7#G5g=y-I74&IM^7wm#g8K)i&QRkbR>oNo}8*_b|1KxvODp zUbzeN%&^3$D!dl zJ&C8UGzgxuiWaf=?#s%kPveBxvzHSRyvOrv`JyM=Ga5#!HLj^ZUp}i?Hj0sU^54xT zCu*-lFB-fdJ!H<~?fB)7)%s`03$dn7`kzk`qAN+%wKL}5%?ZY9?n-}Z7X}?mYM+;~ zX*u%8N;ZIA@Hv2er`0Tv%e&N=eOuw6NcJ53L*x9!I#gMAt_RIXcc1>EH zSxvlu0t%$llxCu0yshE5#nN7HROPY~4n8M2WhzVJC{=R#eJmFkd>HSHUmxYsM>z)d z@#^-)JIG)CTB^n}K4o~;(#o~az7?V3)2AOL{zAFp?Dfz-n+SVp8|OfidJ8RuqkO%U zUXF&zYl^?aA1hsH(->ctpM9>FDv#E?#F&pzUU)*B9n{d2 zo4>fa)N7sL5G_lB2*wM|H6kQ10fd@ZM-Jbf)2GPh-(f&3L`#uU^r)|xiN_(XO2pPvudQD@v57zsE`k!ryl>JWWN8M5Cz4;-2nZ{E#v&6^+= zLmKj)pQ~qmib;P)Z&Z}|u&Myhm-mcIS{yT)1Nwrs_4Ej_wkjVyIo^V>PoW7_V=l}O z!iL4HVlP0~5l3#?oUYq+HtuxO*~)A_NPL8|wPqPmM&u1h;Rf+}$6pL~aUBZ``Qwm8 z*|))%h0mbAd-V%!tSbFmK`mLeP@3@YnFQ||;~`1AHzr=aRtpnFw)8^lAdWQmK3nzG z32BUYv(lPLuYMXfc7ZbItEIGO(o)x{qJ#C51Ey(Uh6hIAilYsQ?wtY6toH zQbQs-)~VbSz7$+&iBfb&QCKFJ_o*a_Z!a=PgE=gP!Vk0uK|B#_4TuM2O}ui}k4`Cv z{nfVe?|9iQZwupF;Z*_?zmd$hOf7%H-R393CPoisZ47RW^_iQ#Ae`mDRQGnsH)W*9 zzEqN|KkS+^+?(B%Tz%9x!fU#`St%t<02D?%Gb|h_w<889D_#oW7Fe4pcXB3h<Bc~OIAgUYJ{D(sCk$^+Jo+Q6yc;&N%Q(gnqDhv3_;$WqDGS`Ya|VmkK2DZ z=AMj2m(yqz97o36u5x+Gi#~;`+p7?{gs;@r zXUeR6<`;wch43khpF~RL@dktL}rDBnKun+RgRg)SCJpJmKZbJroM+Xr)&}b$dqLNP6cLTT>~FTiyQE zj&FgrSh3|ai?9$(p7X_6T-lxKAiQ)9T;yyDN{zeh+v9>^_*!v{bxY-TUMh*-Wwf#6 z5_WvqeO+tY3HyTl>Gk*VW8qAE2N8bjqlIu@+E9N4C*;{}aqNijYJWVe_!wnc(7FcZ zXz+Dh-z-v{*Y{R}$AWE)G6FMQG`eSA-)XjtAl@cIZbo^3c`a!ScWv@480Uy>hz_Mx zD12{b8uw@73+hr9AEqpqSay9e1j$3K&lqon3Dp&)IGBd?^;M>QueGl3_9EM6pykz} zcdll{K{0bwM7=7TvS>5*lmJz@-2qMY<0e&0e8NU^%8kdi54}Vvt~+_IKNVD)`!L}4 zVf$*2x-z$K8eS>c?at;+yRmNFWc|^J``t@3x;?cnvzU`50%WH&zFM@#n>vX@jwJ>M zK?v$F4=WyvwdF`uSmKfBW)I^NM4so7LunJkPnB-X^=F_4+!n=L`e#ZSYDQ}tdQMx6 z4sVG)3ky{s4X~s*SP)U8TGRVD#!+K^gcG%DsZaYIxM&IeI$Fvi zu#1f1ArPNdzOj%jrtdzP^x~`00_bak&hcxuu+XFHF?1?4p-RmueGbkS_(BD1Z^UIXCc! zv}5l27!Dhe<%Eeb*s=zCfP5aN!lSw04r2t^vGt`BH4R*&P+nz66XKd$<=E4Vgit=% zuq(>Mb$tAcxk`WeDLx-OWI;K@M=(eoyIS&bPr(us`en2kTmSqzxrWvr-a!wGRHzPyOL5`U~&W-jc(R?U)65`q9)}_XBbS9k|=cn^Px2$2NJI zFnVI_%FIn!V|z@g32a8vWl^l8{(d(^&`C4)1gesHiQsDgM0mHnz48sr{e?Q+RdRLL zfz~Wrf9nKaad8)if_ab9k%Hhf$gOke(V;cI=iYPW%Auw{-{wZYAF`%ibi~XouQcUW zF82)HG&ZhuB?Kx{ZErab8o-wpGeed;Er9TwOP2&y3seN`kv&_cLwLsLgS{Ve-^}A% z^~#=X37=(~Rbs}T$kVCn{kPl#J5J26k+Oik8xO1pBk35AHB*+I)s$OsW*K*ED80Mn zjfYF^odk?c2(TC`b1xR$b5hR@vEjaI_81p8n5*s9nI-9TbpImd z5zDxsb;8l%l|KFcgV=NPnfyk~#@*L3$%AE_5U-QsaWkjx$+x0-MZF?i8|kstSV4k$w|0vn>C1v%Akc|CM&hU{kgaQ z36}Cx(Lb29rHdV+n{a?3{zwu`G4!zPRd9;du59z7WJ~SY?moC0aQeZ}Oz~F=se!?; z%q^>8A3M5eXBvcn<0JE~cO)l1``C>A{y6!+Mj*gkn$rTR?^7S+F^_3H>pdl|J^kxXyu_5qr7)hb}>Gq*B4gtun%4ru@*25;{70cYn%gn zXc6b+2!aw5z;tf!7s>9_R#x9xA;ypT2)R6Eep=m?mra z+3UUMgH8MG$6Ggc;QHx$32>#bX)b9p?%BYFUO1HW8o{aUeANfrhrBc#LuU)hrd~tG z%ZF+H1MtN$scM1iMhD&nxW}>;ty8;l4GZ^QvHoJedbJ9vv4YIIk$S0qwj6P5ww#u@ zFsIHNNn2@gbQv6reWR5)goGe%1bLNw42Wy6z`j%Y$YsfL!6A?J6%QFNqRnd4$zd1z zaI0`0F!)HTnZAP((%O_nPdd!7O0)HpY#Mq*dPD4d1TjfC43bR(r{DuJt9|-dQ6@fT zzb`TJ`gG3#xrQuRZhCW4qH^GHVJJuZ<2{4g35jRY2It%in*!Jrxnad$_3BSgKKVFm z)jjB(szCh|er5?sq*YN!;Y&N%#I58fpFq(pT*{8Qc&9nVCF0o>?2Veuc>Ryy6~+NJ zaXiE~K_2*R+E!aBVtE;fo>CrB(&?HZqrR!b!vz+j^P?lPSn859(O54>Y@m<#?VM79 z+vJ(m&R!fk7E7WsjbWImCyjRYdF3FDx(`lH7c4SaweO1kOkq7{;=g~Kc6JO3va3uS>c-9 zwst?G`vGoqakq;4&hP9`baf_p-)6TKfGyn-QpK)MQKQ01iVLl5w7}_iqswRDp!=+> zH(G(Z624&RmeGc8)n|89zE|FbD{K~2I*Kj-ddzb&Qr`+2z!pg%^%{eP8?ZEAxf}uf z1QEJrwVKH7b?N6p9H0nSgTu>o#~$t@%?fLA1NU&lRu|o(dscDlmj+Ud2otjM;h_p9 zaGx#-`WYTI=sqYb{$$+b@HGOMi~|IxXhm*c-%(@YI73 zj^hbDe9v1!A)X+ctPnNitg(8qiaw_^3#6}mctr&Fog`u{(Y2}02T)b-(|2>N?(5hF znseIK=o&itL?v)n9K4TY)gXp`PDn)^^ud1%Of8o%(nLZ!NiiSyL`o#OJs^UV@RHY# z-FOLHJcW?C1suYDizz1a(a;fDu|Auv*OZH>^D!9>J^2=6FVy+M7irG7i6nnb#*gB6NC!AWgx#wA@%Fi-Oc&#oxHwdFVgH=?_O6o0rPC#oZ#|pt|=00h6pf$ zkLxaEXh=XMfx2X-&ocgs%ZKPtv9*r~c|I|QrDH97+1r-#ZaOqnx;At{xyp2k&91%E z+>H5_m8qPhtxL|mE~;oTNqChw8ajx^}Af6Ik6XtqjZGa^t&e9CP^krVN7Bp(%l((uYrXM14Xw=`R2Rdg-;q(>i1| zh^Ne*PtLaX9a!=3Fc8z$<>DRl0q4K|`BBvePlGunq8_nu)@9xpXRjP+Txu%N;yHb# zCiD=$^!n0N5NB}qy~f$4atf#ct)B3*9sr8r11HHlJ0evU`>wy!kRIlRB6c27bGK9S>EP9`;!b@+#|2!4mWGPsCEZkQ^^thsIS6v-e`6U%Rap$>3l)CImx=_s%U!VWy(Ya8>Nkah|DE3 z#^$CpR{3vBMX7MVf;E_U>R*DzPg>8Y^HL{2@P*=J&r9aXPF7IJOW}=D@z2j)=k|Bx zKV{!@D(0+pC?0WQI!{Mz@8oRnYLEe=Zv9Du}y#o?1f+`rj)ZEuEyRoTSX_&|P7 zW$mmsfwy8{_WV@N`;$`3WgRPL_8gKi_v3bHInSsc9=e60SU3sxVv6U**}Gq!%x&k| zOR*l9z0kOZ?atkj4*Mc*Sa4h%ZhKySa!;pG@z=7=C@Nlj<7!t)_R}YGM$45=ozBK+1%$^Vu^vpEmZY^H7E78DBE73&ke7u)BC?I-8i! zWIW^`HVFS>v--D6Ei>@FU&7j_((wV2Xl%81noj2*AznMRRQ>ly9s0-K{y)=UP2F&HZ&LBVYPL*nT<~MX{!~ajZ z%lJ+$>#D?cRsJH~6szCcdif9z-xVXI21y5}r%wIDQ+8fTmZFXBn$Go;C z`~Uv%b8yR|*E61#f)eJ4{MTv!{eWp;(nNGa@BjCwBu~boM(lce#FGR8@ZP7Dq?ixIN_^|Ps+vXYmX(qtb$!UVoh|C4RVD|s<`2uA(Tg_jt z09-!2Axz&ykTVzC{r2wlcoWg+USa3%3fToxsjE?~nw+WsUB7vaXro8fv zZM?E{(X#DL>KH#C)wx_SUUJxR>!y(U=eNiF$>gGnP_qI_=%yQ(@Oy78?-Pcm<#Au+mTtDQEIOg%x zaPhs`_P-a?FB7lf`YLmoBi3Y&iKMJm7qMbdCt1td{{)P5Q;A}^}Bre<9 z*hEa=MW$r-3aNixAEq1?hqLmYyG-?HeRRfgx<{%%96nDKE?CF!Jyk!FZ25l?&)t(Wus^S+UGsb&`2z>EZSIiz3nyfM4dA79YGq%4G6U$&2thvji2My{NZ7k{NbTymGpEp@}$&JYBhMp;*%CbLx%cnVMLqmnisn!@T}E^of@L$jt`P zlN_DSrA)bYMT0h@2g2l);Aqki4C}K5g@n`d61fJY1;_nk9ZS{n5)}+Fw92`92kBuU z7!5!|i^O&-b7JgmPzw2QeO`;RL0U`6kW88r#3d{muhyC(0Gu8tny5JOYQ@J$$a^X) zqd(p_*oF?<`z})-t}jbtqhe1$B-oUku>;X_ahe>Nt60=5Fa8g!nfZ7N z%uiQUvahhzGlcjRR+asx-xsYo-J(?wBFVA(oV=HU2|mJb;$*uzdcY+ZADV}>8bSBh z=ao+nke43Wo-xWK50>KUHRWe|UZb0pylY_~-&m_01lJuQ6HpS}5ZB@!r>Cs=VF*>e z%$op^t(Tl;ua1=r-YW-z%pN46c(K|7dnEh>&mqKMvV8vg9bBKE3UW&{E0Ro1%-b3I zcGdFX(pxq;g+Co7Ny79+&93*N=8{d$#eO8Z^VpO=W*xGS0g~`qye^m5BcFf@&F9vM zzMAyiD=SI9`T;bkzKmHIh5%gJOQ`d1EuMrO7>O2`D{Bz%iUcA!1;s0!ho8xn&lW%x zI^-+YEr9m>Oc2De2`EL6Cagw7-xREy4i!R?~U)-s; zP>G+9*P$j>ERzWfBf%1N8WPxpz5?HLK*FYEtS+;&*35hMsPbxS*qh{J->vQ-*4bU) zAmbL$wGw4~V$A+6JPu6!K-WM;{8H-h>GshYKNCp-WpV8 zZi1Od(HlK>PEztw`gkR2C>+RNsF=aW$5%=)LkOC$Vg4OmqJg+Zlvqf2ZfNj__v8O= z)nkvfLZQk0}NmKr-&iU~g){JoyzA)az@&A;QuOIdf1l)=UM1Pl zJ>rY~MP(<2FI#eQXS;c!XC5;E$UDPkt9{pWFX(T4^H2*G(aEltXCi=0fWb}H<;a;= z7FL^i570DYyzfwuXPC|NcK++!``fY59b#wITkA$TX_dVk{?Gm3tjWAyoO)P{uj)zm zFvaB_ivo|j{fACD`^CAn?P>$mT6dfW0a?;tEou%yzVoQN!%}QpIMILn>`$wF)1#Op zx0DF!P9j@cb~;4b$XoT3^pFv*LtgywFHT0CN4wuz&fi%jOViX7>gJdHy#%{Lqz4Z| zx|jQ}AEY4PhRWnRcOHyKc)E4RAn@@vYyiyTbL3&;Q#v=RJpU;idAcNAa&DsS-#1;= zcDV^17kMr*hWkR_mjS{(YpgRqW7RNPTiRZP&7EoJ<;n z&@85Q{6)W3GdV_-XGi&{h|9k99u7xdUfqKJj5Q4#w#CDngN<7pZSJjPN+MayNc#I_s%Zb<@ONQ?SPM6m57Mu}&LXj8lS^6nKy{0ib|K^H%J*)JfkVfi*k$X3WIAWc6 z+2_3)=9-luL;_pkL_qBRf9?B*U*0&)OSTlFgx|Y=MU$EnMWUO11jXSq=%KxqxGa@E(FFEVC?pY)2f4wO25^ikD&+a$lk0usA zt#iYsmzS#V*LIb3#dp7|MjpvTa9})!gBavv zhFYFFhv<{pkpr^Mc;{Y=fT-c@0-OtN-P5H%uD4a6`gB^#k|G3yD=amLm7pWCi-S4E zUc%CwpT8%t2C-Z$cFRbnI`)E8vE_#ov6n4Usp>~*r$MDLJ{0YFMwD#&_S;7z<~y@f zxt`J`<)oz?p+Qt?QGWl_u$EjGs>9@fqFY7l6ZD)bENs@Xeo=jke-pcn*&2H}>);sV zk5$^B5`wzEg4JJ=R$5A4Bo7z%k=eY{L>(=Remz3```DjQl?TwRos&+(1~=Z5$+2e- zx9eFfugUZoiB6#>jYFFVs%+3fxDbb+;8@_q0L312`AgTk?mPL~vC4rr>GAddqo*dPftH#SYY`S4+pwV zaAIt9?ESX0BSpWt;D3eIvJ|FjC?YTAnW;fvD?{LJEh;nS4Yc(_Z+GqW%dbxWxMt(4 z@uH_-vjce830ytCH8l-bHY7G9En2r|OFG1LP@2qi3Ls6C_|q1TcRz(ogJxT3Y;sSX zR&@JsSq+ur^bvLnc>MU1VdZO7b_?C?E42q}=%G1`|el zPBG@x?E{DQ`#w4hFv*UvssgJ9%Yk?IT8EEPMi>k%EG)i_L@OGL`AsUj6ri+yNP{qk zpq~ipf37XN%K#N%t^C7;nGS3YGP)1Iy=BM8oGwDHbfLFM36iG@Mrnkcg4MgR^^*G3 zQk_@gr9_v~RdoNjK2}C!xzB>6S-L|Lfa+YXS(0`NQEYIs3)wLmkcDBP#4X8L&a^dE>{CMa+{Zan^03qQhpdY|Z7q z@*8lit>}-YeZqL*CLhZ$E(H#LSHqqBb7ECknS1nk?Y?E-44h5F9!>Xl{n-fq2pMN~ zZD0uD8yJEiJ&WbF@15g92oQ7oo6!&uU74MArAcP7cAn{T25m0ujCGUAYNIT!4sZtQ zqP67C)7fVrW7P;GG8a53bU$7`BcZFTyEfIA^mMs|o{?U#acffN^EOF%2G_9v{`Gx9 zH6T>rLSv49e7vW)sLP$+KccgS4z3^7XhuElp^Gi(Q=j$WO#rjiPu$kx$|b;6jslvNo&hm z9<&o_H_eN}de6S?g?dqJyWh}YS{Pl-;(Q?d1m^7YYZ2Ojcu8rgA!ZG$yjLua@|}q$ z!o>Fu0d4w%&&xl6T_1KK$0)zxqLMiQi%?5}v{9l*W?59}F45$#QDU_y@UY`g$xD1d)rb*Y&Pk+rLmpu$6D9`<%e^ z5KfdFEE^C(09x4?aJ3`ZW{Xno^H=ZA-ug!U7^Wah8Y21GV+YX=L6EZ6&mmGTx3TqT z-EB+|E4j!hDsSdB&VJ~evq}WQ@6m`mWhwBJT2Z|FK=SptjsB^Bn$MriZ$}sa0LY%m zcQ?Tylr+12dDv+f4+DJ$==pQ9xAE+~nfu|R-`?=E!9EHsB|c@g_2Q<+XfbxrEk2DJ z-sQa-sc(#f^;=mR7I{1!xKC0Z<7pMK^1IxbIs1 z?LcnPXw!R<18xlt3y+$44!DY%6&xqeps)>@FSzOw$`QHtDPXXbhDMjMgE0mzcJ}tq zONL{Hyz){*WlX?M%wNelAc2`~27mVS84Rp-D8L?~wG(`hJu*O4ARLah;NIa$9y;E8nSGqVNcFEz~&7u}|IVIPg zSx zLdX)o2Cyi;klckpIabWe#E>UqT`u0T@3$|!?mE|tf?zX+VAs|S=pdqe8UJ)a11chz z>h^RuS6Q;pgic^_Ka>?lLZAI-_)Z16>me=12We_m3(@M0%qft@ui(BM0%(_i_Ac)G zpR7kJQRUw*d5+X&&>2wm=b5JYXi5eN;d>07DrFq{ib^iB=u-1n`%=0CXkI;u^@?40 zUI8M_YGL9ND*p~j{Uwrt)8z)qYo3!<($_Xio^Kpf{!JW7$M<;vucTP*+x51t0J^4` z_CA05p0If_MP=fRPzD?mkLJ`+`;$TAvgc`3N~(Op1dnp2iW zhY3<;QirE1hf>EihOLkP3gYxL9eT|+`ezSDMb7q z!<uuwlk(s_ZMqEBL^1BS;PSa z%u{z+enq<%kyx3gPF%AB6cVkNcZ2AtOCS#-DfE>CQ-s2*88m&H({D1%{ReMmryGvp zm0o838j*io($-O&_AxSICWfw7IHhTh^;J5Gi-TcAJrKz@r*rZ5zT8l+FGy<`d#McJ zrRhae&AK0Re)ei}#OgVIM0Gk@q&420fYG~GcwO-G$zd|zDnPSrT%T^+L5yB(q86{O~~LeDC0(6Wsf1lbe|2&X_`D%YaC9a4Sy3q;pYxEw6zhNCyU_f za41fdQlx+oKPSGw-9&K!YX9^h?Mo~t%8{0J+oRp<$PqGWPAh-WTi z;gmOk8#iA^oZF?C8^UxF(WA`VqDO-bW$WgQj6~1=X6#is_Y2*!1R09KNM>`%@m#*p zF^WoBx_Dgh|6t~_SAIiTrS`pNV)hLxX2Tv#I#DNiZh4JShM{^}+U4&?gK7;>3461C=e*=rrD%tH)wCGqnABPHYOB-n8}msqYSr z0{;tQOs{Puddqr(>o?t4S-ML3Yt;~iMx)BSBW&!ik7%4Sz&~%D zp1bzH-`w2gj=J!|(`b4U^fk*Xf z3tyc3ACiJ}D=c^24@-~_NjUJo6|?cf zNWfher|`ymv-|~Q_5*naVU~wVJ6WrVXI_rzVlJXLph}@5&xx@6LcBrjGVEEu-02nu z9wB;C?AW7+1Cl{|e3&J@vu6#q5S~ka4vG}fmpJW8Qg@@ohQ-U>mw$c6bxVl1nraJ~tlR7hBWkQER0Uo%d|$-S}M7 z9&igPOuN2I>oJ{o{q0=d*rr{~7w9;&6NEx0amc`(&zCFV9He5v_pm2KUjHZggjl zB{80&%|X|^2X+s52DfgagmnZ~1+j}vL^-EVFWpWq+kJFaI^j5h|`_E3Q6c z`67o>o(t`+kail9KX$O{%(EE!Dq()8w0Fs3`Dpe=0ya$}T7N#M=kUf;Jy9Vd9(-a3 z^rYhQA)34??!svp>n3F1zi+<*&|@d!gZer+D@5FI69YVo*G!kOOuMbRkwQR7+K>|Lg~M4b)uQ?$>Z z)WwR(c0jL&Q7p*F!RC)&t&-_g^Vxw?RE9$GP3GNcxmQXblH;eC8Q)e&+#RY2s(9^$ zr7X9fO;?xp-(V$33Yo+9uQ)7D-8HPR%o2S%yB!+JyyAm53XtH92JVBYo(iq06QPg> zgNubx&p)6h{oiMQU=`~?TVf!tpC zt-(T<2KCm+Eo^#&?jh2%ojDoA$w6Z;(xiZ*_wtyAsr)qi*u^E`Bw9`jmFZF+?LrYv z0ln%H@Jw*9Ed7G#p?;6{WNVA&4h`1b;mng)=G~1iX0(k|@G5#sp<`!-{qgEyR=Pyv zXXF+P`P{j4fGBtRivm)Yvt(E|&V%=_RSG5p>_+|lPpGuv5peLq?VI9c$&uH{h^i&p zmJG#d+8b!-I20O|2_%d3!lG!ZgKZ_nU3MKaYokT7%KEilD^R19y7?kWbeBW;GR)Oz zP0`qUFeml$s&Z)^`d;g3WhyGykV1x{G>06z+2Uo)TaI9uPxn6u(m0tQ3-r~NFRI)Z z;Y~5u4L0w(6pzR5?+(#N+65sv?Hf~~?!@Gk_@?fEjWs2hl+&%YpGpkF*}KibupY}V z-_O9N;>MFNL{#S%--!-gx;?5LOBA)2h=;9RS^cyr%`c~K4rsuzOVub{DCz^Hs&Pht zrf~VY(AHEOhZr6%1 z#^LF^r}#(`-|Mn!5z1FdjEJbP$E5zTDFfWnH;(tyeGZ{)kn0bDBY7*S4I$VA?~2MX z$?^>{3ry%%hGl-WI_)qhN$pF)32qAalH9kaYbC-zW@RLhz3SOZC3i|+AC zf*ZX_;Hs7ITRqKVe9c%f*WnOy&9(7M%vpWS1Rn~GOU}N>s@xa+9xkx@QzJzcp9b8LZbd##|Xohcwb$ zCS`i=GIXh;uOxqIu*+otJ;XyS1)td1gMa=R)HMeOmxjcYE{bn%AeSr2#dSdDw?pB;lORZN$}mN4 zm1}sVCK!s!D-L~rO4ECPnI0P=EXrnP-L|{;n|?2NI+C$T@c`#J|p^Aa&b-_58sw8V&AS@Jbw#vZQyq84f5D(k^wuNn0c6i?4L=4 zJ>zT7=Y4JMTh0+O8HnsMNkc2LR{k-MH^FXkCac})I6!hlcCib z+K_QAW+56}4W9x^zL8JzF0vStTTJ2NkD4D~Rxq|II2L@8a?a>{* zX%}PT13{A}qu&Dgl)ixkchehWNzBSTp5(S;!9O2!4Lq&3t$)j~m@T82G-Qq1lh-`b zWfVt`WiuaF5e=a%=A%w|PVQ{|4DuWLw7&$IblHU}UN{}!{?R3ukEy%nNBrCD)o{hg z*b>t*(3KKaeqIm4BI6mBJMhL+c{Xfm9WLrH-SmhINao;}W-;`#xwa-{O_73m%E~%! z{o;ZQ>E3<#TqdprOQ}REm#x*J4@tT8oCw&##tU#f8mrfm9d}1hOCdznLUj)RF{B3n zakp^>Zi0>=y6ig6p7}D!yevx|hw9GfYq)>(INfix+F4PE-hwvd6!I(G4bRuW4KPF* ze{~ssjcc0h*`wV2TN;l2fK8|ppYZ|T$7ne_y-Qd^G3c6WTL)S^41;=|)?B;?B{`7S zYq_XhGj_l4Ofq|AZgEN^dRCQ1kJ?qS-Q68@(7eDUd_wh}vYzGi#N&;T2{#hEIGMZ%|JD#<3vs0c z&Jgm7U-g&aTHo<6yi05{0ZWlZ46%zc&SdYHtAu34}*8|4Tto0n_2ZYSA1k&8m|9xma} zLM_gkB-|5HxA0o=F22}JiIK#LpY@iI*7rgmx`C3y zMOEvTer)`u?yNg(SLL_d&sN^H(9_^@H0NMb$Kghepngx$ZeOLti_JKkaVSzQvC0gb$wBs) zm^8FXy4OZeOn$2|I_5y@P%s{C>hToisBe{3*0PvL_EnrVGP)#&c-&Q5O65gM*~T{- zbID~i3CutVHkoRZw|tEhrU3p>Gr#T31_Cag=w}ZchE( z4uhBp{5Mp7TvYG(L`Tg8xJR`3Ua_<->E@m+2i07lPunXU3C09SM$ODy4B8Qba{!Eq zMyQ$yY{;dfcVfv~sTk#XxTL$vG-ItaaNIx?n(GMvZ?{RkaYqvA=@yGKA&vW0`%lEpj z?{(eJqc!kyg9VOmRTNlDW#@C3Rx4r&O4&~ogW{P)q#AA8q^b!TP*UVe_xP%TBHTij zZqMP2N2$vpBe^-o_P_C*%aWc>#OH(2@v+rLOM1OSP<}9a$bYEDQPq3MRw+%K;tiQ) z@xzCsn_lkCYPQ4Y@0D5Y+F)fTZZh6I{1TizYC+-E=)?KrPwz*fXf0|!ei{H~yR-?j zp_eiTrF6E-oX^~o3!Boki9=u_9)Fj)+&ME~G|JSjKET=gSya7vb3^6a^}_5U*R7jw zz4v;B73;~6wIrQj`}x(>a$&=m<8eluRwo))l)+E*w_GEejj_MK7^S8sWlb?#6-dIG zb0(F*=+|5lBu?dv()m?fkwj_s+t><(yI(S%I#Nc}@lq%9jmCLr88=Id&f~zEYF0#D zq_w12)FdIt`^Q(8WDaN5jQrg>yQrMj9u9lDnIBg4FYh}*t!{m{bxZaIuRZeJH!bmY z@Tp7abA#2uR7{I*?m>)DDEXYZ$pYvoCM0_jJ_9i!7|9?K~}@(ui|4MoE9AR*oy=W_qe* z2cG-PtgVOP97wF25mr_6l04N)hY13%+H!e$eqlPz?#nsDSyR8&R-HiuhegPk0=`^G zSj>&PdT3~b4yZ~YPlHWM5z#VnI#3em^md%55FlhJw*p8Ci=C$OH6svqC_cAO45cJ1HTcK%;eA} z-K-f0YXp<^FKG1LVjKdCj)E`ByElafoF`ExYjfRGd9$S!QXWE`Wwhs_m8}qVP)IPB} zNcl2I+P{R&a6R5YVlL?z+yzwB0ObKkK*-z^@kJuFf>@dsz!Kn=fLiTSuN)8*c`*tm zXa5_0&QAtBJoNc!nJIVCjNW3zj*`pAV^Rm8#)+T~1lgh_cDiJ?vm1 zOy^4V$S0RaSr@l&2N_$yS=6%-l?El-bR(?!&pEjsR&2<$qc^zhJ>^ zh5?0lmvptgs*Y9H!Dg<5{D3)fsvnIcahT7F`>C__$>xo&wgO%&T{!?n8qTX!z^O4s_33|)M!HZ{QY}`q>fo)@-pe=`CPPKTnh1O* zVRu&ZBuCJrXe2G)>rfQqxeFJ&^E`>+78}b@u-qnPq!GL^i@{(r zQpdaUCNmP;Kyk!tntQA|?065F+_!N+dO!FImG|A4)DsImnknhekoPz{#~bCs3I=1) zO=@TU9@Nr3Le7dBohQNzkX<((Xtn}?U^pX4r zCb}4Dv$#%D?gW4qVSnu;&N(lGg+1sdDqjl5)>)3aww15AcJ+otNBi9ZADMkz>*#>FVW?R@XnnWzF=jO1ZEpj4jWc4O9lS z5xuR`6G0hmS*NscE|oouC~`=WJ*17kmPY<0twz2D|o69MR3ME2iAU+iXsm-V0~Uv!6N1^=Z5Ck7ASEvs92{60J|&&ghSTPixdK zMhd`ZdKr1ZX;(A<&quc%gUW0!bhG=l+CSk`+)?#BZlWs>2s=^{4k=y`|Er8Ee}F-C z6D@{Xmp)IEpVPrUM(v5P%$|!%1!hZbFY!zUU&hu`UoM~Buw@a(M=Vv;{R2`#c=2a{#76>1? zE6I3EBJdcM?9ofXCmVt};OmGQ@5;|Y^7<9$SVHa5%!^=(LkV&VP?;mW4#DKI2}wpz z9*V|UVL57G9A?>)zIAufBkpqi9r=Pn;sd2;{JE?)toFM?MvyR)T-wLCrgvs>=g!fV zP`Pg2q}lOaD}U_d10a5v6}ZTnTUXk{4|=PMo5)NqpR5b5@hmii8!`*ay*Q4fKBb*SLF`5prwX3aB-Us6ZGA?4u9QIE? ztS4!R>3{%tX4P1lA<4vX$$!+5EHLKa1$_VBCgbl9zjxP;YJ&A3xi`r8>C4-H%%iE4 zcaSgc>BY6>CjUI{t?T4ZdCL~>8I)@jdxT8+dwnTU`R#k~)s;=8Om0ok;7EsgDtb2? zm5_-iZ|pIs_v%Q}N?bjG-!SvF)Z^%DXAdxX7!*DiQ8~ufn&H(Kxl$CBkQxq5|LW^- zXtAbs{k$&+FK2{qJFETIJ`-0DET!zVhZejx&Izg*+KI=mmq^;G_7)>%u*vr6GOkF? zpgX1-#yB?0#SYGl^%dW9UC6^_x{7g>a6f+M>yKG0j`1`a^N$K1uPvi)$Q?i$lose7 zH8{=>N9q8z^d`j);Y11tAq|Vd;o3rQPCr9Ye6@;3? zf*f_X1`2H!33n+cOj%{g1f$f8obky41)G@AoJ&*XqueWlc4uL1rzV|1aaaVA19uDsOdb-m+ncQ0z2#Ts23`(Sx`eUd;9 zH(W;BEz6BiMo2}M@LG(LEW{5~w0MX5V@EN1*A!|N4e=4(HNS6_9}PJCcz4|Y^ljcO zH-&sc!o#a0&yl+eG1)(Ovdpm4=;#*L&B#@XZfJ0 z98?w@=?(P6-ii2|+B9esqu4UiErvLuc|oz-$1W@n(xj&)dSs~gGkoCkC7xU=jJ)xo zr7~QX+JQVS%`aNG{|rH#h+qadw^VxmhT45o0o{~u>!^XpyAHLB>sVbcgUObLxu7_j z+n{i-w6_mqfRZQoja%Yfv7LFPrWoD5p-B z7-_i%z8Hv|Brub(!cMuqs;;ZO!=;gg&oq})WXbu#c`iw$%<8`Nj@_Hs{Pq>l-6!*| zwqXjT>V8iGdRQTE`p%IL&Xn+9CGQ4OtI*Za9%4dXrHsqcL%`^aJNi85pgV$HEkm%X zt<`nwq8we1?;Y-Z)xx(gdRdu%tX^obDnN}VC@SN1UaYiurD)Ln~MhODIvBWImg zmz19(?VL(C1{%Gg@u7wZ=plKt18*XIFnL)=dhw6EA$FWp+&vdO~= zHMXtaCuzrW#iVQDvyvi*76Jit(Y+TGPwU+k`}?Ifkk-r_?j!A}mJJj5aWnfkSr8Wg)l9pLyb@ub%QvB_`%?cGs?zm8)e=^ z0&VrFz_fk>rU4+_WMhSbEDMC(6Wn>g zMyUcwr;D`QNi}RZ!d>tTUCEGeZvBSeeS?3&Fd%(-Cy@QDOFjlrK7b~q`AZS{vOK_H zU9Ev!dlJk{0t*wx)qeAA%Zv%Q|mizsreIle1fcQlh~*Rva|TdQ&p9C`|_ zWb&g``5<1BLy%F9PJel{l2=)DqqM!TkA`b`6hm=Lib??e&-?v;VN9+x+x*+~@+kR2 zXp}HL;Yg6J%|H#ab!uLHzs%Jef02Zu)!+jX+%swHc=Bpp6}2NXY6vfSOz%)rG7cf1 zkp@?`ot(UR;`_NvD~E-RQB80&W~byTTJ zODZ+E>z(=LYZj&gevNtnjXpS4V~4B4NkW!E1UPHUHz9l(w5Vifg<+p@Uw#<2s-0<> z^ivcFF9AA=T2JEmoF03jPt66s6QBN@qbzSQ;fnImcg)L$#ihri_bFv1_8LLw7CTZs zN)ka_nrsEiimx#?M{C-X5|V~E6>X5$tZ)bobRnQCIv!wYtOF1Cj2HP=1_4XZm$~nN z=hPHAa$RK7{oUauv8X~Xe)=qUf8QvOxDlid1Fow`yzKYi54CPUDI|cnd{{&VQlnpx zRJJb{gr>PUF)htR>SeX9MZ~ns)SOKO8nu@A<|i2r8vpK+2;k6ySEGra2e!U{fr`b7 zGiIG9SL!kns{0pzntGQopkzg)Zrgor;kf%DGNY8kLh;?u=#btIiTe}*d)|oR*HZ|^yvVanN)RfC1!}+^DDy^uba~(j1@@5;ls)jddjpB0Tk26?TuZ0L?(9)8k z|61_BGpJR`LScyPML+?=Z4pw)ub+wI%#<_LowQ8FA5~IHt6<-M?n4Hwo2&6y*!TJI zxo!M9njP+lyo#~{5U82!%<|V5Slp;A`+0b?sG(ZGK?+p9_%G**3{9ZExhwL~exSz1 znLJt&NPo#EdK*d-hH4Pll2Ypdj3qz;>mhb`vt1s&>`ntgUhjNpDwPhR_&eM%OY^i2Ys@O zKNL8my%J1*xcxPBpHtU6pKlmClm~s1JFor1?^w?0?9*E;&8MqUyodr>61rRsY{(T76f%6W;)IW^tfe^$7&IJW*X@zSG z7|hTY@B09MY4f4cjJ6dsK#|QW=QN(Ir{Bzx(_YEY*BPtjjxs5t_f&H)-!cqimzmVR z0hDbrDBgAZ{L#9HQ3#fo%wwg#UyMUX&*+mwhDpV%4gj0rs>k^dl&qn-EN&P#gCz*9 z-PB-hLqA)9het%MZ~jqPZ`eqyO4_n4^PH{=4}qlY;Lxp*QR;=~;~DkO*~qz#>pu9E z+P(tF3KR_Zi2qZdIV8>3<0ypJCQI1tJiY#~gQn%dq!Zw+HuPxC+9YjL!()m;AUXUH zc+D(xPp5_ZfMphcU!U)FxtXhn3{{TM#Y?&vO~U-c^dMdJ^|wL$R{~F|J2BChL*G@2 zuc3C;t?Ydn+-x~yCLbFdd{kl7owru;m{1r)(RZGtwgHSxS>sOy#YM+0v$JA+1V75+ zay@mgkn88CS6h`7Z(uTRKky#1eJ^6M`~wQ4IsSoOUxL<;acbL|a~G2xfum zgME({G$a7K76Sd8W}SVKq|-#6HbaFo4c8|O-XtGyT>%H54mSR^5RVFt8ci>_d{B^o zA$Q54>2#n(Fy$USYc@DxqY~~IYPt%T_5@^00k!HsqCu0x>oFdt1kDcGSeXT731mNq z^}^VOiQ3gH!s3Qm>-(3sczBuNB>*oG^dGZU|FH0U7WtAToGp#1UoWWcS1_9+lu8 zv!O*V?GFw-w@)Q(?+TFuZ-g#$=ZEbNqK1G8ym(c~V*=_34TOUW$!E&tzFR)o;(Pes z>AgzTkNHL18f!a%N6;5x9Gbmn(?Ry~wc(=J2`>yPK*FdX%DVpq3=O^|&bK3v(C~EI zUqjBTAS?uUovOOi8X};F@xsClX+t{=;qhdgL+KZH0J*kewwVwFaR8@o&AIxFzQ+&_ zE6E%+BYMVh;uT4{uZ0j|7Jp+voD~8<0b8IB4f+7_dz~wZrsaJzLkj!4dH9n z!AAg}6&C|Aaee4e>T`HyajByv04jxKzk^Jx7>fM8YNs2GM_mf}<6R5_#ufkxnopt) zD{K_}&4!M8I{N0`E#J~hq32tE-eHE=vNGYI1bUQxpm#ewf2NLu2;`?rNVaxQq^{lx zsLF6)=5q19Iuk$Na=(b3+4z&Tb6}|}v-=@s6@o1(N-ylO1iNjV1S&znzCT*6 zPvZl@Y&FMSK%kZR2<<^+hekCH|0D$9 zx))5n7M|~)+1EG5wTvioDrrw*$%`}rIq{siF2Ii4Si!L4?(Kixa66mdE!^hfbBv%~ zPp}VvV5ImUdrocGr|vK0TeF~$_Fj$&3eds`mied!82E6zPW{SHm_EX)TMdR7`*lk8 zvV4d)*5lZ|>fQ?sMU59m)EwyqjF8AvZ*3e`k_?IuerG&kW#feCsd%}UXgzZ+YPnt9 z&Uze$rdH9FZsx)kI`FiWF0Voe0Ri@s6)xobkn_9&x=-=NycxXFTi65oNb%bCLVynb z&4zgT@#D~&H~!_fo^Jajci*jJ`To$v@)pto;iMN>%@K?zZpE;Qk9;|IwHWsPq5Ez@ z=ymLi{~?ELKje()rQ)2EyoDAeMf?)B8!AbUu+0tut{*;w2{=T&;HqkOom@UuuNHR~X$idY zcJikimMWz8PIsOvjPHej@0OK>&9UnAh0hRzu)&IUeF`_Py&KfnAWAd_Ts5rr`_EJI zAu9-QG1*UOcZfU_I^Rhp;Uuo3+-WJaH=&1iYr{s;iH`$rCVbFvDkBsvOs|^~^g zI!Is8Pl-gB!;_(_RZkCpIk&UrQ`=aA)o|)huE3a-|i~zq9R{nJ*W4| zw$ZGA`yuuJ58aO)whV&OjG;A7P9Qr}U^Jy}rS5XQpia?QV3kipu_`Loe&d2T^5$W$Z)cDBPBI^M zQpM{gaG>u@j~N!KoA0tf$k30%m*We>xF+Y@f~VWg4wf_m)mV{Zv>&!Ot!GUHQh>EA zpK0G01?^7B8;0X%tKjP%z9X&+&*K3A6^vH(d@e9sKf8%t(wcd6im) zk&f|8oJ$A(hDuuIlsUDC9hBO=`jF<9hilJ&Gqm`|M^A^|w92urud-29P`iGBsKZ?H+4?y#IXWek-i zV`TDljhejYdTMB4?9hd2KBhL>Rq01_#_ zrvAP_Mg$0tx&q-jAoDRY#r7M&Joc*4S7{=w`QPsW{8s@d1je_|C)dwLt&8=pD{!!% zCA)_Y;=7fE(HM7SElh*5y`>>%AF&SvMTG{k)P$N?K0)kszCi3t#V((cm!`r0uwrmA zI0M3*3~GC#&!G+#!LCf{ZJO9o-jhD?1htOkI-6r&cVQO?rf75oB zz5MB(nh$<=v74@ZepW{DKVUPeM$>O$pOSt!PuQM0vx)k5>Ym|~M60DUbek#g{f6NK zO?ZKR+Dc1B&XcgGjK)2W+?3hHl2|^fMdf_^b_Kg|w}M8^k7I7X zF$YF>nSV4eJS=adTD7IR{*Jb{zW*xUAP9VXqBYO|vpVY#ktPJE?})zD)-MTLKErCX zSH0R24)dScJm8z!;3WJ4K4qDj5~$M6b=u2;H~nw4&u_lxzEgZLw7sRXXT>Z2Td7TV zEZ0h@f{fp;>aH+X~zdX~*pQ ztM3cl3>m+6nPHk?T?YdmcI2bFOPj~rBb=2-zN7~)F^n17t*v15p#->A|G&`&RCi$++lv4p*UXX)c~< zk!rvF{oAcEPt2SfOiX3Gp6}`T$9*jE_3%>mM~(6hwcY!=+GgiqXm#6YW|-<}KPjrW zIbB8m(f9XkcC$fd*uR@S3(c`%$CUFZ7sLgP=*164TiF?(A6bkhZvuXNyaG=)dz}61 F{{T$lidX;u literal 0 HcmV?d00001 diff --git a/docs/writing-stories/tags.mdx b/docs/writing-stories/tags.mdx index a56e39b97fdc..4b74ac3616d3 100644 --- a/docs/writing-stories/tags.mdx +++ b/docs/writing-stories/tags.mdx @@ -56,13 +56,13 @@ Tags can be removed for all stories in your project (in `.storybook/preview.js|t Custom tags enable a flexible layer of categorization on top of Storybook's sidebar hierarchy. In the example above, we created an `experimental` tag to indicate that a story is not yet stable. You can create custom tags for any purpose. Sample uses might include: -- Status such as `experimental`, `new`, `stable`, or `deprecated` -- User persona such as `admin`, `user`, or `developer` +- Status, such as `experimental`, `new`, `stable`, or `deprecated` +- User persona, such as `admin`, `user`, or `developer` - Component ownerhip Custom tags are useful because they show up as filters in Storybook's sidebar. Selecting a tag in the filter causes the sidebar to only show stories with that tag. Selecting multiple tags shows stories that contain any of those tags. -