* build(#10336): init
* fix(#10336): invalid name conversion
* build(#10336): load locales and vite config
* refactor(#10336): remove unused imports
* build(#10336): separate definitions and generated codes
* refactor(#10336): remove hatches
* refactor(#10336): module semantics
* refactor(#10336): remove unused common preferences
* fix: typo
* build(#10336): mock assets
* build(#10336): impl `SatisfiesExpression`
* build(#10336): control themes
* refactor(#10336): semantics
* build(#10336): make .storybook as an individual TypeScript project
* style(#10336): use single quote
* build(#10336): avoid intrinsic component names
* chore: suppress linter
* style: typing
* build(#10336): update dependencies
* docs: note about Storybook
* build(#10336): sync
* build(#10336): full reload server on change
* chore: use defaultStore instead
* build(#10336): show popups on Story
* refactor(#10336): remove redundant div
* docs: fix
* build(#10336): interactions
* build(#10336): add an interaction test for `<MkA/>`
* build(#10336): bump storybook
* docs(#10336): mention to pre-build misskey-js
* build(#10336): write stories for `MkAcct`
* build(#10336): write stories for `MkAd`
* build(#10336): fix missing type definition
* build(#10336): use `toHaveTextContent`
* build(#10336): write some stories
* build(#10336): hide internal args
* build(#10336): generate `components/global` stories only
* build(#10336): write stories for `MkMisskeyFlavoredMarkdown`
* fix: conflict errors
* build(#10336): subcomponents on sidebar
* refactor: restore `SatisfiesExpression`
* docs(#10336): note development status
* build(#10336): use chokidar-cli
* docs(#10336): note chokidar-cli mode
* chore(#10336): untrack generated stories files
* fix: pointer handling
* build(#10336): finalize
* chore: add static option to `MkLoading`
* refactor(#10336): bind to local args
* fix: missing case
* revert: restore `SatisfiesExpression`
This reverts commit f246699f38.
* build(#10336): make storybook buildable
* build(#10336): staticify assets
* build(#10336): staticified directory structure
* build(#10336): normalize path for Windows
* ci(#10336): create actions
* build(#10336): ignore tsc errors
* build(#10336): ignore tsc errors
* build(#10336): missing dependencies
* build(#10336): missing dependencies
* build(#10336): use fast-glob
* fix: invalid lockfile
* ci(#10336): increase heap size
* build(#10336): use unpkg for storybook tabler icons
* build(#10336): use unpkg for storybook twemojis
* build(#10336): disable `ProfilePageCat`
* build(#10336): blur `MkA` before interaction ends
* ci(#10336): stabilize
* ci(#10336): fetch-depth
* build(#10336): isChromatic
* ci(#10336): notify on changes
* ci(#10336): fix typo
* ci(#10336): missing working directory
* ci(#10336): skip build
* ci(#10336): fix path
* build(#10336): fails on Windows
* build(#10336): available on Windows
* ci(#10336): disable animation on chromatic
* ci(#10336): add static option to `PageHeader.tabs`
* chore: void
* ci(#10336): change parameters
* docs(#10336): update CONTRIBUTING
* docs(#10336): note about meta overriding and etc.
* ci(#10336): use Chromatic for checks
* ci(#10336): use `pull_request` instead of `pull_request_target` for now
* ci(#10336): use `exitOnceUploaded`
* ci(#10336): reuse built storybook
* ci(#10336): back to `pull_request_target`
* chore: unused dependencies
* style(#10336): reduce prettier indents
* style: note about `TSSatisfiesExpression`
			
			
This commit is contained in:
		
							parent
							
								
									8a0201fe9c
								
							
						
					
					
						commit
						38d0b62167
					
				
					 59 changed files with 7708 additions and 365 deletions
				
			
		
							
								
								
									
										56
									
								
								.github/workflows/storybook.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								.github/workflows/storybook.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,56 @@
 | 
				
			||||||
 | 
					name: Storybook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - master
 | 
				
			||||||
 | 
					      - develop
 | 
				
			||||||
 | 
					  pull_request_target:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - uses: actions/checkout@v3.3.0
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        fetch-depth: 0
 | 
				
			||||||
 | 
					        submodules: true
 | 
				
			||||||
 | 
					    - name: Install pnpm
 | 
				
			||||||
 | 
					      uses: pnpm/action-setup@v2
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        version: 7
 | 
				
			||||||
 | 
					        run_install: false
 | 
				
			||||||
 | 
					    - name: Use Node.js 18.x
 | 
				
			||||||
 | 
					      uses: actions/setup-node@v3.6.0
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        node-version: 18.x
 | 
				
			||||||
 | 
					        cache: 'pnpm'
 | 
				
			||||||
 | 
					    - run: corepack enable
 | 
				
			||||||
 | 
					    - run: pnpm i --frozen-lockfile
 | 
				
			||||||
 | 
					    - name: Check pnpm-lock.yaml
 | 
				
			||||||
 | 
					      run: git diff --exit-code pnpm-lock.yaml
 | 
				
			||||||
 | 
					    - name: Build misskey-js
 | 
				
			||||||
 | 
					      run: pnpm --filter misskey-js build
 | 
				
			||||||
 | 
					    - name: Build storybook
 | 
				
			||||||
 | 
					      run: pnpm --filter frontend build-storybook
 | 
				
			||||||
 | 
					      env:
 | 
				
			||||||
 | 
					        NODE_OPTIONS: "--max_old_space_size=7168"
 | 
				
			||||||
 | 
					    - name: Publish to Chromatic
 | 
				
			||||||
 | 
					      id: chromatic
 | 
				
			||||||
 | 
					      uses: chromaui/action@v1
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        exitOnceUploaded: true
 | 
				
			||||||
 | 
					        projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
 | 
				
			||||||
 | 
					        storybookBuildDir: storybook-static
 | 
				
			||||||
 | 
					        workingDir: packages/frontend
 | 
				
			||||||
 | 
					    - name: Compare on Chromatic
 | 
				
			||||||
 | 
					      if: github.event_name == 'pull_request_target'
 | 
				
			||||||
 | 
					      run: pnpm --filter frontend chromatic -d storybook-static --exit-once-uploaded --patch-build ${{ github.head_ref }}...${{ github.base_ref }}
 | 
				
			||||||
 | 
					      env:
 | 
				
			||||||
 | 
					        CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
 | 
				
			||||||
 | 
					    - name: Upload Artifacts
 | 
				
			||||||
 | 
					      uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        name: storybook
 | 
				
			||||||
 | 
					        path: packages/frontend/storybook-static
 | 
				
			||||||
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -56,6 +56,7 @@ api-docs.json
 | 
				
			||||||
/files
 | 
					/files
 | 
				
			||||||
ormconfig.json
 | 
					ormconfig.json
 | 
				
			||||||
temp
 | 
					temp
 | 
				
			||||||
 | 
					/packages/frontend/src/**/*.stories.ts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# blender backups
 | 
					# blender backups
 | 
				
			||||||
*.blend1
 | 
					*.blend1
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										110
									
								
								CONTRIBUTING.md
									
										
									
									
									
								
							
							
						
						
									
										110
									
								
								CONTRIBUTING.md
									
										
									
									
									
								
							| 
						 | 
					@ -203,6 +203,116 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
 | 
				
			||||||
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
 | 
					vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
 | 
				
			||||||
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
 | 
					これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Storybook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Misskey uses [Storybook](https://storybook.js.org/) for UI development.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Setup & Run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Universal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					##### Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pnpm --filter misskey-js build
 | 
				
			||||||
 | 
					pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					##### Run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### macOS & Linux
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					##### Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pnpm --filter misskey-js build
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					##### Run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pnpm --filter frontend storybook-dev
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When you create a new component (in this example, `MyComponent.vue`), the story file (`MyComponent.stories.ts`) will be automatically generated by the `.storybook/generate.js` script.
 | 
				
			||||||
 | 
					You can override the default story by creating a impl story file (`MyComponent.stories.impl.ts`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```ts
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					/* eslint-disable import/no-duplicates */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MyComponent from './MyComponent.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MyComponent,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MyComponent v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							foo: 'bar',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAvatar>;
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```ts
 | 
				
			||||||
 | 
					import MyComponent from './MyComponent.vue';
 | 
				
			||||||
 | 
					void MyComponent;
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can override the component meta by creating a meta story file (`MyComponent.stories.meta.ts`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```ts
 | 
				
			||||||
 | 
					export const argTypes = {
 | 
				
			||||||
 | 
						scale: {
 | 
				
			||||||
 | 
							control: {
 | 
				
			||||||
 | 
								type: 'range',
 | 
				
			||||||
 | 
								min: 1,
 | 
				
			||||||
 | 
								max: 4,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```ts
 | 
				
			||||||
 | 
					import { rest } from 'msw';
 | 
				
			||||||
 | 
					export const handlers = [
 | 
				
			||||||
 | 
						rest.post('/api/notes/timeline', (req, res, ctx) => {
 | 
				
			||||||
 | 
							return res(
 | 
				
			||||||
 | 
								ctx.json([]),
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Notes
 | 
					## Notes
 | 
				
			||||||
### How to resolve conflictions occurred at pnpm-lock.yaml?
 | 
					### How to resolve conflictions occurred at pnpm-lock.yaml?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								packages/frontend/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/frontend/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					/storybook-static
 | 
				
			||||||
							
								
								
									
										9
									
								
								packages/frontend/.storybook/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/frontend/.storybook/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					# (cd path/to/frontend; pnpm tsc -p .storybook)
 | 
				
			||||||
 | 
					# (cd path/to/frontend; node .storybook/generate.js)
 | 
				
			||||||
 | 
					/generate.js
 | 
				
			||||||
 | 
					# (cd path/to/frontend; node .storybook/preload-locale.js)
 | 
				
			||||||
 | 
					/preload-locale.js
 | 
				
			||||||
 | 
					/locale.ts
 | 
				
			||||||
 | 
					# (cd path/to/frontend; node .storybook/preload-theme.js)
 | 
				
			||||||
 | 
					/preload-theme.js
 | 
				
			||||||
 | 
					/themes.ts
 | 
				
			||||||
							
								
								
									
										54
									
								
								packages/frontend/.storybook/fakes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								packages/frontend/.storybook/fakes.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					import type { entities } from 'misskey-js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const userDetailed = {
 | 
				
			||||||
 | 
						id: 'someuserid',
 | 
				
			||||||
 | 
						username: 'miskist',
 | 
				
			||||||
 | 
						host: 'misskey-hub.net',
 | 
				
			||||||
 | 
						name: 'Misskey User',
 | 
				
			||||||
 | 
						onlineStatus: 'unknown',
 | 
				
			||||||
 | 
						avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
 | 
				
			||||||
 | 
						avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
 | 
				
			||||||
 | 
						emojis: [],
 | 
				
			||||||
 | 
						bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog',
 | 
				
			||||||
 | 
						bannerColor: '#000000',
 | 
				
			||||||
 | 
						bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
 | 
				
			||||||
 | 
						birthday: '2014-06-20',
 | 
				
			||||||
 | 
						createdAt: '2016-12-28T22:49:51.000Z',
 | 
				
			||||||
 | 
						description: 'I am a cool user!',
 | 
				
			||||||
 | 
						ffVisibility: 'public',
 | 
				
			||||||
 | 
						fields: [
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: 'Website',
 | 
				
			||||||
 | 
								value: 'https://misskey-hub.net',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
						followersCount: 1024,
 | 
				
			||||||
 | 
						followingCount: 16,
 | 
				
			||||||
 | 
						hasPendingFollowRequestFromYou: false,
 | 
				
			||||||
 | 
						hasPendingFollowRequestToYou: false,
 | 
				
			||||||
 | 
						isAdmin: false,
 | 
				
			||||||
 | 
						isBlocked: false,
 | 
				
			||||||
 | 
						isBlocking: false,
 | 
				
			||||||
 | 
						isBot: false,
 | 
				
			||||||
 | 
						isCat: false,
 | 
				
			||||||
 | 
						isFollowed: false,
 | 
				
			||||||
 | 
						isFollowing: false,
 | 
				
			||||||
 | 
						isLocked: false,
 | 
				
			||||||
 | 
						isModerator: false,
 | 
				
			||||||
 | 
						isMuted: false,
 | 
				
			||||||
 | 
						isSilenced: false,
 | 
				
			||||||
 | 
						isSuspended: false,
 | 
				
			||||||
 | 
						lang: 'en',
 | 
				
			||||||
 | 
						location: 'Fediverse',
 | 
				
			||||||
 | 
						notesCount: 65536,
 | 
				
			||||||
 | 
						pinnedNoteIds: [],
 | 
				
			||||||
 | 
						pinnedNotes: [],
 | 
				
			||||||
 | 
						pinnedPage: null,
 | 
				
			||||||
 | 
						pinnedPageId: null,
 | 
				
			||||||
 | 
						publicReactions: false,
 | 
				
			||||||
 | 
						securityKeys: false,
 | 
				
			||||||
 | 
						twoFactorEnabled: false,
 | 
				
			||||||
 | 
						updatedAt: null,
 | 
				
			||||||
 | 
						uri: null,
 | 
				
			||||||
 | 
						url: null,
 | 
				
			||||||
 | 
					} satisfies entities.UserDetailed
 | 
				
			||||||
							
								
								
									
										406
									
								
								packages/frontend/.storybook/generate.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										406
									
								
								packages/frontend/.storybook/generate.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,406 @@
 | 
				
			||||||
 | 
					import { existsSync, readFileSync } from 'node:fs';
 | 
				
			||||||
 | 
					import { writeFile } from 'node:fs/promises';
 | 
				
			||||||
 | 
					import { basename, dirname } from 'node:path/posix';
 | 
				
			||||||
 | 
					import { GENERATOR, type State, generate } from 'astring';
 | 
				
			||||||
 | 
					import type * as estree from 'estree';
 | 
				
			||||||
 | 
					import glob from 'fast-glob';
 | 
				
			||||||
 | 
					import { format } from 'prettier';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SatisfiesExpression extends estree.BaseExpression {
 | 
				
			||||||
 | 
						type: 'SatisfiesExpression';
 | 
				
			||||||
 | 
						expression: estree.Expression;
 | 
				
			||||||
 | 
						reference: estree.Identifier;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const generator = {
 | 
				
			||||||
 | 
						...GENERATOR,
 | 
				
			||||||
 | 
						SatisfiesExpression(node: SatisfiesExpression, state: State) {
 | 
				
			||||||
 | 
							switch (node.expression.type) {
 | 
				
			||||||
 | 
								case 'ArrowFunctionExpression': {
 | 
				
			||||||
 | 
									state.write('(');
 | 
				
			||||||
 | 
									this[node.expression.type](node.expression, state);
 | 
				
			||||||
 | 
									state.write(')');
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								default: {
 | 
				
			||||||
 | 
									// @ts-ignore
 | 
				
			||||||
 | 
									this[node.expression.type](node.expression, state);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							state.write(' satisfies ', node as unknown as estree.Expression);
 | 
				
			||||||
 | 
							this[node.reference.type](node.reference, state);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SplitCamel<
 | 
				
			||||||
 | 
						T extends string,
 | 
				
			||||||
 | 
						YC extends string = '',
 | 
				
			||||||
 | 
						YN extends readonly string[] = []
 | 
				
			||||||
 | 
					> = T extends `${infer XH}${infer XR}`
 | 
				
			||||||
 | 
						? XR extends ''
 | 
				
			||||||
 | 
							? [...YN, Uncapitalize<`${YC}${XH}`>]
 | 
				
			||||||
 | 
							: XH extends Uppercase<XH>
 | 
				
			||||||
 | 
							? SplitCamel<XR, Lowercase<XH>, [...YN, YC]>
 | 
				
			||||||
 | 
							: SplitCamel<XR, `${YC}${XH}`, YN>
 | 
				
			||||||
 | 
						: YN;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @ts-ignore
 | 
				
			||||||
 | 
					type SplitKebab<T extends string> = T extends `${infer XH}-${infer XR}`
 | 
				
			||||||
 | 
						? [XH, ...SplitKebab<XR>]
 | 
				
			||||||
 | 
						: [T];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ToKebab<T extends readonly string[]> = T extends readonly [
 | 
				
			||||||
 | 
						infer XO extends string
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
						? XO
 | 
				
			||||||
 | 
						: T extends readonly [
 | 
				
			||||||
 | 
								infer XH extends string,
 | 
				
			||||||
 | 
								...infer XR extends readonly string[]
 | 
				
			||||||
 | 
						  ]
 | 
				
			||||||
 | 
						? `${XH}${XR extends readonly string[] ? `-${ToKebab<XR>}` : ''}`
 | 
				
			||||||
 | 
						: '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @ts-ignore
 | 
				
			||||||
 | 
					type ToPascal<T extends readonly string[]> = T extends readonly [
 | 
				
			||||||
 | 
						infer XH extends string,
 | 
				
			||||||
 | 
						...infer XR extends readonly string[]
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
						? `${Capitalize<XH>}${ToPascal<XR>}`
 | 
				
			||||||
 | 
						: '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function h<T extends estree.Node>(
 | 
				
			||||||
 | 
						component: T['type'],
 | 
				
			||||||
 | 
						props: Omit<T, 'type'>
 | 
				
			||||||
 | 
					): T {
 | 
				
			||||||
 | 
						const type = component.replace(/(?:^|-)([a-z])/g, (_, c) => c.toUpperCase());
 | 
				
			||||||
 | 
						return Object.assign(props || {}, { type }) as T;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
						namespace JSX {
 | 
				
			||||||
 | 
							type Element = estree.Node;
 | 
				
			||||||
 | 
							type ElementClass = never;
 | 
				
			||||||
 | 
							type ElementAttributesProperty = never;
 | 
				
			||||||
 | 
							type ElementChildrenAttribute = never;
 | 
				
			||||||
 | 
							type IntrinsicAttributes = never;
 | 
				
			||||||
 | 
							type IntrinsicClassAttributes<T> = never;
 | 
				
			||||||
 | 
							type IntrinsicElements = {
 | 
				
			||||||
 | 
								[T in keyof typeof generator as ToKebab<SplitCamel<Uncapitalize<T>>>]: {
 | 
				
			||||||
 | 
									[K in keyof Omit<
 | 
				
			||||||
 | 
										Parameters<(typeof generator)[T]>[0],
 | 
				
			||||||
 | 
										'type'
 | 
				
			||||||
 | 
									>]?: Parameters<(typeof generator)[T]>[0][K];
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toStories(component: string): string {
 | 
				
			||||||
 | 
						const msw = `${component.slice(0, -'.vue'.length)}.msw`;
 | 
				
			||||||
 | 
						const implStories = `${component.slice(0, -'.vue'.length)}.stories.impl`;
 | 
				
			||||||
 | 
						const metaStories = `${component.slice(0, -'.vue'.length)}.stories.meta`;
 | 
				
			||||||
 | 
						const hasMsw = existsSync(`${msw}.ts`);
 | 
				
			||||||
 | 
						const hasImplStories = existsSync(`${implStories}.ts`);
 | 
				
			||||||
 | 
						const hasMetaStories = existsSync(`${metaStories}.ts`);
 | 
				
			||||||
 | 
						const base = basename(component);
 | 
				
			||||||
 | 
						const dir = dirname(component);
 | 
				
			||||||
 | 
						const literal =
 | 
				
			||||||
 | 
							<literal
 | 
				
			||||||
 | 
								value={component
 | 
				
			||||||
 | 
									.slice('src/'.length, -'.vue'.length)
 | 
				
			||||||
 | 
									.replace(/\./g, '/')}
 | 
				
			||||||
 | 
							/> as estree.Literal;
 | 
				
			||||||
 | 
						const identifier =
 | 
				
			||||||
 | 
							<identifier
 | 
				
			||||||
 | 
								name={base
 | 
				
			||||||
 | 
									.slice(0, -'.vue'.length)
 | 
				
			||||||
 | 
									.replace(/[-.]|^(?=\d)/g, '_')
 | 
				
			||||||
 | 
									.replace(/(?<=^[^A-Z_]*$)/, '_')}
 | 
				
			||||||
 | 
							/> as estree.Identifier;
 | 
				
			||||||
 | 
						const parameters = (
 | 
				
			||||||
 | 
							<object-expression
 | 
				
			||||||
 | 
								properties={[
 | 
				
			||||||
 | 
									<property
 | 
				
			||||||
 | 
										key={<identifier name='layout' /> as estree.Identifier}
 | 
				
			||||||
 | 
										value={<literal value={`${dir}/`.startsWith('src/pages/') ? 'fullscreen' : 'centered'}/> as estree.Literal}
 | 
				
			||||||
 | 
										kind={'init' as const}
 | 
				
			||||||
 | 
									/> as estree.Property,
 | 
				
			||||||
 | 
									...(hasMsw
 | 
				
			||||||
 | 
										? [
 | 
				
			||||||
 | 
												<property
 | 
				
			||||||
 | 
													key={<identifier name='msw' /> as estree.Identifier}
 | 
				
			||||||
 | 
													value={<identifier name='msw' /> as estree.Identifier}
 | 
				
			||||||
 | 
													kind={'init' as const}
 | 
				
			||||||
 | 
													shorthand
 | 
				
			||||||
 | 
												/> as estree.Property,
 | 
				
			||||||
 | 
										  ]
 | 
				
			||||||
 | 
										: []),
 | 
				
			||||||
 | 
								]}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						) as estree.ObjectExpression;
 | 
				
			||||||
 | 
						const program = (
 | 
				
			||||||
 | 
							<program
 | 
				
			||||||
 | 
								body={[
 | 
				
			||||||
 | 
									<import-declaration
 | 
				
			||||||
 | 
										source={<literal value='@storybook/vue3' /> as estree.Literal}
 | 
				
			||||||
 | 
										specifiers={[
 | 
				
			||||||
 | 
											<import-specifier
 | 
				
			||||||
 | 
												local={<identifier name='Meta' /> as estree.Identifier}
 | 
				
			||||||
 | 
												imported={<identifier name='Meta' /> as estree.Identifier}
 | 
				
			||||||
 | 
											/> as estree.ImportSpecifier,
 | 
				
			||||||
 | 
											...(hasImplStories
 | 
				
			||||||
 | 
												? []
 | 
				
			||||||
 | 
												: [
 | 
				
			||||||
 | 
														<import-specifier
 | 
				
			||||||
 | 
															local={<identifier name='StoryObj' /> as estree.Identifier}
 | 
				
			||||||
 | 
															imported={<identifier name='StoryObj' /> as estree.Identifier}
 | 
				
			||||||
 | 
														/> as estree.ImportSpecifier,
 | 
				
			||||||
 | 
													]),
 | 
				
			||||||
 | 
										]}
 | 
				
			||||||
 | 
									/> as estree.ImportDeclaration,
 | 
				
			||||||
 | 
									...(hasMsw
 | 
				
			||||||
 | 
										? [
 | 
				
			||||||
 | 
												<import-declaration
 | 
				
			||||||
 | 
													source={<literal value={`./${basename(msw)}`} /> as estree.Literal}
 | 
				
			||||||
 | 
													specifiers={[
 | 
				
			||||||
 | 
														<import-namespace-specifier
 | 
				
			||||||
 | 
															local={<identifier name='msw' /> as estree.Identifier}
 | 
				
			||||||
 | 
														/> as estree.ImportNamespaceSpecifier,
 | 
				
			||||||
 | 
													]}
 | 
				
			||||||
 | 
												/> as estree.ImportDeclaration,
 | 
				
			||||||
 | 
										  ]
 | 
				
			||||||
 | 
										: []),
 | 
				
			||||||
 | 
									...(hasImplStories
 | 
				
			||||||
 | 
										? []
 | 
				
			||||||
 | 
										: [
 | 
				
			||||||
 | 
												<import-declaration
 | 
				
			||||||
 | 
													source={<literal value={`./${base}`} /> as estree.Literal}
 | 
				
			||||||
 | 
													specifiers={[
 | 
				
			||||||
 | 
														<import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier,
 | 
				
			||||||
 | 
													]}
 | 
				
			||||||
 | 
												/> as estree.ImportDeclaration,
 | 
				
			||||||
 | 
										  ]),
 | 
				
			||||||
 | 
									...(hasMetaStories
 | 
				
			||||||
 | 
										? [
 | 
				
			||||||
 | 
												<import-declaration
 | 
				
			||||||
 | 
													source={<literal value={`./${basename(metaStories)}`} /> as estree.Literal}
 | 
				
			||||||
 | 
													specifiers={[
 | 
				
			||||||
 | 
														<import-namespace-specifier
 | 
				
			||||||
 | 
															local={<identifier name='storiesMeta' /> as estree.Identifier}
 | 
				
			||||||
 | 
														/> as estree.ImportNamespaceSpecifier,
 | 
				
			||||||
 | 
													]}
 | 
				
			||||||
 | 
												/> as estree.ImportDeclaration,
 | 
				
			||||||
 | 
											]
 | 
				
			||||||
 | 
										: []),
 | 
				
			||||||
 | 
									<variable-declaration
 | 
				
			||||||
 | 
										kind={'const' as const}
 | 
				
			||||||
 | 
										declarations={[
 | 
				
			||||||
 | 
											<variable-declarator
 | 
				
			||||||
 | 
												id={<identifier name='meta' /> as estree.Identifier}
 | 
				
			||||||
 | 
												init={
 | 
				
			||||||
 | 
													<satisfies-expression
 | 
				
			||||||
 | 
														expression={
 | 
				
			||||||
 | 
															<object-expression
 | 
				
			||||||
 | 
																properties={[
 | 
				
			||||||
 | 
																	<property
 | 
				
			||||||
 | 
																		key={<identifier name='title' /> as estree.Identifier}
 | 
				
			||||||
 | 
																		value={literal}
 | 
				
			||||||
 | 
																		kind={'init' as const}
 | 
				
			||||||
 | 
																	/> as estree.Property,
 | 
				
			||||||
 | 
																	<property
 | 
				
			||||||
 | 
																		key={<identifier name='component' /> as estree.Identifier}
 | 
				
			||||||
 | 
																		value={identifier}
 | 
				
			||||||
 | 
																		kind={'init' as const}
 | 
				
			||||||
 | 
																	/> as estree.Property,
 | 
				
			||||||
 | 
																	...(hasMetaStories
 | 
				
			||||||
 | 
																		? [
 | 
				
			||||||
 | 
																				<spread-element
 | 
				
			||||||
 | 
																					argument={<identifier name='storiesMeta' /> as estree.Identifier}
 | 
				
			||||||
 | 
																				/> as estree.SpreadElement,
 | 
				
			||||||
 | 
																			]
 | 
				
			||||||
 | 
																		: [])
 | 
				
			||||||
 | 
																]}
 | 
				
			||||||
 | 
															/> as estree.ObjectExpression
 | 
				
			||||||
 | 
														}
 | 
				
			||||||
 | 
														reference={<identifier name={`Meta<typeof ${identifier.name}>`} /> as estree.Identifier}
 | 
				
			||||||
 | 
													/> as estree.Expression
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											/> as estree.VariableDeclarator,
 | 
				
			||||||
 | 
										]}
 | 
				
			||||||
 | 
									/> as estree.VariableDeclaration,
 | 
				
			||||||
 | 
									...(hasImplStories
 | 
				
			||||||
 | 
										? []
 | 
				
			||||||
 | 
										: [
 | 
				
			||||||
 | 
												<export-named-declaration
 | 
				
			||||||
 | 
													declaration={
 | 
				
			||||||
 | 
														<variable-declaration
 | 
				
			||||||
 | 
															kind={'const' as const}
 | 
				
			||||||
 | 
															declarations={[
 | 
				
			||||||
 | 
																<variable-declarator
 | 
				
			||||||
 | 
																	id={<identifier name='Default' /> as estree.Identifier}
 | 
				
			||||||
 | 
																	init={
 | 
				
			||||||
 | 
																		<satisfies-expression
 | 
				
			||||||
 | 
																			expression={
 | 
				
			||||||
 | 
																				<object-expression
 | 
				
			||||||
 | 
																					properties={[
 | 
				
			||||||
 | 
																						<property
 | 
				
			||||||
 | 
																							key={<identifier name='render' /> as estree.Identifier}
 | 
				
			||||||
 | 
																							value={
 | 
				
			||||||
 | 
																								<function-expression
 | 
				
			||||||
 | 
																									params={[
 | 
				
			||||||
 | 
																										<identifier name='args' /> as estree.Identifier,
 | 
				
			||||||
 | 
																									]}
 | 
				
			||||||
 | 
																									body={
 | 
				
			||||||
 | 
																										<block-statement
 | 
				
			||||||
 | 
																											body={[
 | 
				
			||||||
 | 
																												<return-statement
 | 
				
			||||||
 | 
																													argument={
 | 
				
			||||||
 | 
																														<object-expression
 | 
				
			||||||
 | 
																															properties={[
 | 
				
			||||||
 | 
																																<property
 | 
				
			||||||
 | 
																																	key={<identifier name='components' /> as estree.Identifier}
 | 
				
			||||||
 | 
																																	value={
 | 
				
			||||||
 | 
																																		<object-expression
 | 
				
			||||||
 | 
																																			properties={[
 | 
				
			||||||
 | 
																																				<property key={identifier} value={identifier} kind={'init' as const} shorthand /> as estree.Property,
 | 
				
			||||||
 | 
																																			]}
 | 
				
			||||||
 | 
																																		/> as estree.ObjectExpression
 | 
				
			||||||
 | 
																																	}
 | 
				
			||||||
 | 
																																	kind={'init' as const}
 | 
				
			||||||
 | 
																																/> as estree.Property,
 | 
				
			||||||
 | 
																																<property
 | 
				
			||||||
 | 
																																	key={<identifier name='setup' /> as estree.Identifier}
 | 
				
			||||||
 | 
																																	value={
 | 
				
			||||||
 | 
																																		<function-expression
 | 
				
			||||||
 | 
																																			params={[]}
 | 
				
			||||||
 | 
																																			body={
 | 
				
			||||||
 | 
																																				<block-statement
 | 
				
			||||||
 | 
																																					body={[
 | 
				
			||||||
 | 
																																						<return-statement
 | 
				
			||||||
 | 
																																							argument={
 | 
				
			||||||
 | 
																																								<object-expression
 | 
				
			||||||
 | 
																																									properties={[
 | 
				
			||||||
 | 
																																										<property
 | 
				
			||||||
 | 
																																											key={<identifier name='args' /> as estree.Identifier}
 | 
				
			||||||
 | 
																																											value={<identifier name='args' /> as estree.Identifier}
 | 
				
			||||||
 | 
																																											kind={'init' as const}
 | 
				
			||||||
 | 
																																											shorthand
 | 
				
			||||||
 | 
																																										/> as estree.Property,
 | 
				
			||||||
 | 
																																									]}
 | 
				
			||||||
 | 
																																								/> as estree.ObjectExpression
 | 
				
			||||||
 | 
																																							}
 | 
				
			||||||
 | 
																																						/> as estree.ReturnStatement,
 | 
				
			||||||
 | 
																																					]}
 | 
				
			||||||
 | 
																																				/> as estree.BlockStatement
 | 
				
			||||||
 | 
																																			}
 | 
				
			||||||
 | 
																																		/> as estree.FunctionExpression
 | 
				
			||||||
 | 
																																	}
 | 
				
			||||||
 | 
																																	method
 | 
				
			||||||
 | 
																																	kind={'init' as const}
 | 
				
			||||||
 | 
																																/> as estree.Property,
 | 
				
			||||||
 | 
																																<property
 | 
				
			||||||
 | 
																																	key={<identifier name='computed' /> as estree.Identifier}
 | 
				
			||||||
 | 
																																	value={
 | 
				
			||||||
 | 
																																		<object-expression
 | 
				
			||||||
 | 
																																			properties={[
 | 
				
			||||||
 | 
																																				<property
 | 
				
			||||||
 | 
																																					key={<identifier name='props' /> as estree.Identifier}
 | 
				
			||||||
 | 
																																					value={
 | 
				
			||||||
 | 
																																						<function-expression
 | 
				
			||||||
 | 
																																							params={[]}
 | 
				
			||||||
 | 
																																							body={
 | 
				
			||||||
 | 
																																								<block-statement
 | 
				
			||||||
 | 
																																									body={[
 | 
				
			||||||
 | 
																																										<return-statement
 | 
				
			||||||
 | 
																																											argument={
 | 
				
			||||||
 | 
																																												<object-expression
 | 
				
			||||||
 | 
																																													properties={[
 | 
				
			||||||
 | 
																																														<spread-element
 | 
				
			||||||
 | 
																																															argument={
 | 
				
			||||||
 | 
																																																<member-expression
 | 
				
			||||||
 | 
																																																	object={<this-expression /> as estree.ThisExpression}
 | 
				
			||||||
 | 
																																																	property={<identifier name='args' /> as estree.Identifier}
 | 
				
			||||||
 | 
																																																/> as estree.MemberExpression
 | 
				
			||||||
 | 
																																															}
 | 
				
			||||||
 | 
																																														/> as estree.SpreadElement,
 | 
				
			||||||
 | 
																																													]}
 | 
				
			||||||
 | 
																																												/> as estree.ObjectExpression
 | 
				
			||||||
 | 
																																											}
 | 
				
			||||||
 | 
																																										/> as estree.ReturnStatement,
 | 
				
			||||||
 | 
																																									]}
 | 
				
			||||||
 | 
																																								/> as estree.BlockStatement
 | 
				
			||||||
 | 
																																							}
 | 
				
			||||||
 | 
																																						/> as estree.FunctionExpression
 | 
				
			||||||
 | 
																																					}
 | 
				
			||||||
 | 
																																					method
 | 
				
			||||||
 | 
																																					kind={'init' as const}
 | 
				
			||||||
 | 
																																				/> as estree.Property,
 | 
				
			||||||
 | 
																																			]}
 | 
				
			||||||
 | 
																																		/> as estree.ObjectExpression
 | 
				
			||||||
 | 
																																	}
 | 
				
			||||||
 | 
																																	kind={'init' as const}
 | 
				
			||||||
 | 
																																/> as estree.Property,
 | 
				
			||||||
 | 
																																<property
 | 
				
			||||||
 | 
																																	key={<identifier name='template' /> as estree.Identifier}
 | 
				
			||||||
 | 
																																	value={<literal value={`<${identifier.name} v-bind="props" />`} /> as estree.Literal}
 | 
				
			||||||
 | 
																																	kind={'init' as const}
 | 
				
			||||||
 | 
																																/> as estree.Property,
 | 
				
			||||||
 | 
																															]}
 | 
				
			||||||
 | 
																														/> as estree.ObjectExpression
 | 
				
			||||||
 | 
																													}
 | 
				
			||||||
 | 
																												/> as estree.ReturnStatement,
 | 
				
			||||||
 | 
																											]}
 | 
				
			||||||
 | 
																										/> as estree.BlockStatement
 | 
				
			||||||
 | 
																									}
 | 
				
			||||||
 | 
																								/> as estree.FunctionExpression
 | 
				
			||||||
 | 
																							}
 | 
				
			||||||
 | 
																							method
 | 
				
			||||||
 | 
																							kind={'init' as const}
 | 
				
			||||||
 | 
																						/> as estree.Property,
 | 
				
			||||||
 | 
																						<property
 | 
				
			||||||
 | 
																							key={<identifier name='parameters' /> as estree.Identifier}
 | 
				
			||||||
 | 
																							value={parameters}
 | 
				
			||||||
 | 
																							kind={'init' as const}
 | 
				
			||||||
 | 
																						/> as estree.Property,
 | 
				
			||||||
 | 
																					]}
 | 
				
			||||||
 | 
																				/> as estree.ObjectExpression
 | 
				
			||||||
 | 
																			}
 | 
				
			||||||
 | 
																			reference={<identifier name={`StoryObj<typeof ${identifier.name}>`} /> as estree.Identifier}
 | 
				
			||||||
 | 
																		/> as estree.Expression
 | 
				
			||||||
 | 
																	}
 | 
				
			||||||
 | 
																/> as estree.VariableDeclarator,
 | 
				
			||||||
 | 
															]}
 | 
				
			||||||
 | 
														/> as estree.VariableDeclaration
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
												/> as estree.ExportNamedDeclaration,
 | 
				
			||||||
 | 
											]),
 | 
				
			||||||
 | 
									<export-default-declaration
 | 
				
			||||||
 | 
										declaration={(<identifier name='meta' />) as estree.Identifier}
 | 
				
			||||||
 | 
									/> as estree.ExportDefaultDeclaration,
 | 
				
			||||||
 | 
								]}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						) as estree.Program;
 | 
				
			||||||
 | 
						return format(
 | 
				
			||||||
 | 
							'/* eslint-disable @typescript-eslint/explicit-function-return-type */\n' +
 | 
				
			||||||
 | 
								'/* eslint-disable import/no-default-export */\n' +
 | 
				
			||||||
 | 
								generate(program, { generator }) +
 | 
				
			||||||
 | 
								(hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''),
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								parser: 'babel-ts',
 | 
				
			||||||
 | 
								singleQuote: true,
 | 
				
			||||||
 | 
								useTabs: true,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// promisify(glob)('src/{components,pages,ui,widgets}/**/*.vue').then(
 | 
				
			||||||
 | 
					glob('src/components/global/**/*.vue').then(
 | 
				
			||||||
 | 
						(components) =>
 | 
				
			||||||
 | 
							Promise.all(
 | 
				
			||||||
 | 
								components.map((component) => {
 | 
				
			||||||
 | 
									const stories = component.replace(/\.vue$/, '.stories.ts');
 | 
				
			||||||
 | 
									return writeFile(stories, toStories(component));
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										35
									
								
								packages/frontend/.storybook/main.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								packages/frontend/.storybook/main.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import { resolve } from 'node:path';
 | 
				
			||||||
 | 
					import type { StorybookConfig } from '@storybook/vue3-vite';
 | 
				
			||||||
 | 
					import { mergeConfig } from 'vite';
 | 
				
			||||||
 | 
					const config = {
 | 
				
			||||||
 | 
						stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
 | 
				
			||||||
 | 
						addons: [
 | 
				
			||||||
 | 
							'@storybook/addon-essentials',
 | 
				
			||||||
 | 
							'@storybook/addon-interactions',
 | 
				
			||||||
 | 
							'@storybook/addon-links',
 | 
				
			||||||
 | 
							'@storybook/addon-storysource',
 | 
				
			||||||
 | 
							resolve(__dirname, '../node_modules/storybook-addon-misskey-theme'),
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
						framework: {
 | 
				
			||||||
 | 
							name: '@storybook/vue3-vite',
 | 
				
			||||||
 | 
							options: {},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						docs: {
 | 
				
			||||||
 | 
							autodocs: 'tag',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						core: {
 | 
				
			||||||
 | 
							disableTelemetry: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						async viteFinal(config, options) {
 | 
				
			||||||
 | 
							return mergeConfig(config, {
 | 
				
			||||||
 | 
								build: {
 | 
				
			||||||
 | 
									target: [
 | 
				
			||||||
 | 
										'chrome108',
 | 
				
			||||||
 | 
										'firefox109',
 | 
				
			||||||
 | 
										'safari16',
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StorybookConfig;
 | 
				
			||||||
 | 
					export default config;
 | 
				
			||||||
							
								
								
									
										12
									
								
								packages/frontend/.storybook/manager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/frontend/.storybook/manager.ts
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										16
									
								
								packages/frontend/.storybook/mocks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								packages/frontend/.storybook/mocks.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					import { type SharedOptions, rest } from 'msw';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const onUnhandledRequest = ((req, print) => {
 | 
				
			||||||
 | 
						if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						print.warning()
 | 
				
			||||||
 | 
					}) satisfies SharedOptions['onUnhandledRequest'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const commonHandlers = [
 | 
				
			||||||
 | 
						rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => {
 | 
				
			||||||
 | 
							const { codepoints } = req.params;
 | 
				
			||||||
 | 
							const value = await fetch(`https://unpkg.com/@discordapp/twemoji@14.1.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
 | 
				
			||||||
 | 
							return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value));
 | 
				
			||||||
 | 
						}),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
							
								
								
									
										9
									
								
								packages/frontend/.storybook/preload-locale.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/frontend/.storybook/preload-locale.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					import { writeFile } from 'node:fs/promises';
 | 
				
			||||||
 | 
					import { resolve } from 'node:path';
 | 
				
			||||||
 | 
					import * as locales from '../../../locales';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					writeFile(
 | 
				
			||||||
 | 
						resolve(__dirname, 'locale.ts'),
 | 
				
			||||||
 | 
						`export default ${JSON.stringify(locales['ja-JP'], undefined, 2)} as const;`,
 | 
				
			||||||
 | 
						'utf8',
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										39
									
								
								packages/frontend/.storybook/preload-theme.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								packages/frontend/.storybook/preload-theme.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					import { readFile, writeFile } from 'node:fs/promises';
 | 
				
			||||||
 | 
					import { resolve } from 'node:path';
 | 
				
			||||||
 | 
					import * as JSON5 from 'json5';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const keys = [
 | 
				
			||||||
 | 
						'_dark',
 | 
				
			||||||
 | 
						'_light',
 | 
				
			||||||
 | 
						'l-light',
 | 
				
			||||||
 | 
						'l-coffee',
 | 
				
			||||||
 | 
						'l-apricot',
 | 
				
			||||||
 | 
						'l-rainy',
 | 
				
			||||||
 | 
						'l-botanical',
 | 
				
			||||||
 | 
						'l-vivid',
 | 
				
			||||||
 | 
						'l-cherry',
 | 
				
			||||||
 | 
						'l-sushi',
 | 
				
			||||||
 | 
						'l-u0',
 | 
				
			||||||
 | 
						'd-dark',
 | 
				
			||||||
 | 
						'd-persimmon',
 | 
				
			||||||
 | 
						'd-astro',
 | 
				
			||||||
 | 
						'd-future',
 | 
				
			||||||
 | 
						'd-botanical',
 | 
				
			||||||
 | 
						'd-green-lime',
 | 
				
			||||||
 | 
						'd-green-orange',
 | 
				
			||||||
 | 
						'd-cherry',
 | 
				
			||||||
 | 
						'd-ice',
 | 
				
			||||||
 | 
						'd-u0',
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Promise.all(keys.map((key) => readFile(resolve(__dirname, `../src/themes/${key}.json5`), 'utf8'))).then((sources) => {
 | 
				
			||||||
 | 
						writeFile(
 | 
				
			||||||
 | 
							resolve(__dirname, './themes.ts'),
 | 
				
			||||||
 | 
							`export default ${JSON.stringify(
 | 
				
			||||||
 | 
								Object.fromEntries(sources.map((source, i) => [keys[i], JSON5.parse(source)])),
 | 
				
			||||||
 | 
								undefined,
 | 
				
			||||||
 | 
								2,
 | 
				
			||||||
 | 
							)} as const;`,
 | 
				
			||||||
 | 
							'utf8'
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										4
									
								
								packages/frontend/.storybook/preview-head.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								packages/frontend/.storybook/preview-head.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.12.0/tabler-icons.min.css">
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  window.global = window;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										113
									
								
								packages/frontend/.storybook/preview.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								packages/frontend/.storybook/preview.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					import { addons } from '@storybook/addons';
 | 
				
			||||||
 | 
					import { FORCE_REMOUNT } from '@storybook/core-events';
 | 
				
			||||||
 | 
					import { type Preview, setup } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import isChromatic from 'chromatic/isChromatic';
 | 
				
			||||||
 | 
					import { initialize, mswDecorator } from 'msw-storybook-addon';
 | 
				
			||||||
 | 
					import locale from './locale';
 | 
				
			||||||
 | 
					import { commonHandlers, onUnhandledRequest } from './mocks';
 | 
				
			||||||
 | 
					import themes from './themes';
 | 
				
			||||||
 | 
					import '../src/style.scss';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const appInitialized = Symbol();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let moduleInitialized = false;
 | 
				
			||||||
 | 
					let unobserve = () => {};
 | 
				
			||||||
 | 
					let misskeyOS = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loadTheme(applyTheme: typeof import('../src/scripts/theme')['applyTheme']) {
 | 
				
			||||||
 | 
						unobserve();
 | 
				
			||||||
 | 
						const theme = themes[document.documentElement.dataset.misskeyTheme];
 | 
				
			||||||
 | 
						if (theme) {
 | 
				
			||||||
 | 
							applyTheme(themes[document.documentElement.dataset.misskeyTheme]);
 | 
				
			||||||
 | 
						} else if (isChromatic()) {
 | 
				
			||||||
 | 
							applyTheme(themes['l-light']);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const observer = new MutationObserver((entries) => {
 | 
				
			||||||
 | 
							for (const entry of entries) {
 | 
				
			||||||
 | 
								if (entry.attributeName === 'data-misskey-theme') {
 | 
				
			||||||
 | 
									const target = entry.target as HTMLElement;
 | 
				
			||||||
 | 
									const theme = themes[target.dataset.misskeyTheme];
 | 
				
			||||||
 | 
									if (theme) {
 | 
				
			||||||
 | 
										applyTheme(themes[target.dataset.misskeyTheme]);
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										target.removeAttribute('style');
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						observer.observe(document.documentElement, {
 | 
				
			||||||
 | 
							attributes: true,
 | 
				
			||||||
 | 
							attributeFilter: ['data-misskey-theme'],
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						unobserve = () => observer.disconnect();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					initialize({
 | 
				
			||||||
 | 
						onUnhandledRequest,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					localStorage.setItem("locale", JSON.stringify(locale));
 | 
				
			||||||
 | 
					queueMicrotask(() => {
 | 
				
			||||||
 | 
						Promise.all([
 | 
				
			||||||
 | 
							import('../src/components'),
 | 
				
			||||||
 | 
							import('../src/directives'),
 | 
				
			||||||
 | 
							import('../src/widgets'),
 | 
				
			||||||
 | 
							import('../src/scripts/theme'),
 | 
				
			||||||
 | 
							import('../src/store'),
 | 
				
			||||||
 | 
							import('../src/os'),
 | 
				
			||||||
 | 
						]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => {
 | 
				
			||||||
 | 
							setup((app) => {
 | 
				
			||||||
 | 
								moduleInitialized = true;
 | 
				
			||||||
 | 
								if (app[appInitialized]) {
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								app[appInitialized] = true;
 | 
				
			||||||
 | 
								loadTheme(applyTheme);
 | 
				
			||||||
 | 
								components(app);
 | 
				
			||||||
 | 
								directives(app);
 | 
				
			||||||
 | 
								widgets(app);
 | 
				
			||||||
 | 
								misskeyOS = os;
 | 
				
			||||||
 | 
								if (isChromatic()) {
 | 
				
			||||||
 | 
									defaultStore.set('animation', false);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const preview = {
 | 
				
			||||||
 | 
						decorators: [
 | 
				
			||||||
 | 
							(Story, context) => {
 | 
				
			||||||
 | 
								const story = Story();
 | 
				
			||||||
 | 
								if (!moduleInitialized) {
 | 
				
			||||||
 | 
									const channel = addons.getChannel();
 | 
				
			||||||
 | 
									(globalThis.requestIdleCallback || setTimeout)(() => {
 | 
				
			||||||
 | 
										channel.emit(FORCE_REMOUNT, { storyId: context.id });
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return story;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							mswDecorator,
 | 
				
			||||||
 | 
							(Story, context) => {
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									setup() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											context,
 | 
				
			||||||
 | 
											popups: misskeyOS.popups,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									template:
 | 
				
			||||||
 | 
										'<component :is="popup.component" v-for="popup in popups" :key="popup.id" v-bind="popup.props" v-on="popup.events"/>' +
 | 
				
			||||||
 | 
										'<story />',
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							controls: {
 | 
				
			||||||
 | 
								exclude: /^__/,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							msw: {
 | 
				
			||||||
 | 
								handlers: commonHandlers,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies Preview;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default preview;
 | 
				
			||||||
							
								
								
									
										22
									
								
								packages/frontend/.storybook/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/frontend/.storybook/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						"compilerOptions": {
 | 
				
			||||||
 | 
							"strict": true,
 | 
				
			||||||
 | 
							"allowUnusedLabels": false,
 | 
				
			||||||
 | 
							"allowUnreachableCode": false,
 | 
				
			||||||
 | 
							"exactOptionalPropertyTypes": true,
 | 
				
			||||||
 | 
							"noFallthroughCasesInSwitch": true,
 | 
				
			||||||
 | 
							"noImplicitOverride": true,
 | 
				
			||||||
 | 
							"noImplicitReturns": true,
 | 
				
			||||||
 | 
							"noPropertyAccessFromIndexSignature": true,
 | 
				
			||||||
 | 
							"noUncheckedIndexedAccess": true,
 | 
				
			||||||
 | 
							"noUnusedLocals": true,
 | 
				
			||||||
 | 
							"noUnusedParameters": true,
 | 
				
			||||||
 | 
							"checkJs": true,
 | 
				
			||||||
 | 
							"esModuleInterop": true,
 | 
				
			||||||
 | 
							"skipLibCheck": true,
 | 
				
			||||||
 | 
							"forceConsistentCasingInFileNames": true,
 | 
				
			||||||
 | 
							"jsx": "react",
 | 
				
			||||||
 | 
							"jsxFactory": "h"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"files": ["./generate.tsx", "./preload-locale.ts", "./preload-theme.ts"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,9 @@
 | 
				
			||||||
	"scripts": {
 | 
						"scripts": {
 | 
				
			||||||
		"watch": "vite",
 | 
							"watch": "vite",
 | 
				
			||||||
		"build": "vite build",
 | 
							"build": "vite build",
 | 
				
			||||||
 | 
							"storybook-dev": "chokidar 'src/**/*.{mdx,ts,vue}' -d 1000 -t 1000 --initial -i '**/*.stories.ts' -c 'pkill -f node_modules/storybook/index.js; node_modules/.bin/tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js && node_modules/.bin/storybook dev -p 6006 --ci'",
 | 
				
			||||||
 | 
							"build-storybook": "tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js && storybook build",
 | 
				
			||||||
 | 
							"chromatic": "chromatic",
 | 
				
			||||||
		"test": "vitest --run",
 | 
							"test": "vitest --run",
 | 
				
			||||||
		"test-and-coverage": "vitest --run --coverage",
 | 
							"test-and-coverage": "vitest --run --coverage",
 | 
				
			||||||
		"typecheck": "vue-tsc --noEmit",
 | 
							"typecheck": "vue-tsc --noEmit",
 | 
				
			||||||
| 
						 | 
					@ -71,8 +74,27 @@
 | 
				
			||||||
		"vuedraggable": "next"
 | 
							"vuedraggable": "next"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"devDependencies": {
 | 
						"devDependencies": {
 | 
				
			||||||
 | 
							"@storybook/addon-essentials": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/addon-interactions": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/addon-links": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/addon-storysource": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/addons": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/blocks": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/core-events": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/jest": "0.0.10",
 | 
				
			||||||
 | 
							"@storybook/manager-api": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/preview-api": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/react": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/react-vite": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/testing-library": "0.0.14-next.1",
 | 
				
			||||||
 | 
							"@storybook/theming": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/types": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/vue3": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@storybook/vue3-vite": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"@testing-library/jest-dom": "^5.16.5",
 | 
				
			||||||
		"@testing-library/vue": "^6.6.1",
 | 
							"@testing-library/vue": "^6.6.1",
 | 
				
			||||||
		"@types/escape-regexp": "0.0.1",
 | 
							"@types/escape-regexp": "0.0.1",
 | 
				
			||||||
 | 
							"@types/estree": "^1.0.0",
 | 
				
			||||||
		"@types/gulp": "4.0.10",
 | 
							"@types/gulp": "4.0.10",
 | 
				
			||||||
		"@types/gulp-rename": "2.0.1",
 | 
							"@types/gulp-rename": "2.0.1",
 | 
				
			||||||
		"@types/matter-js": "0.18.2",
 | 
							"@types/matter-js": "0.18.2",
 | 
				
			||||||
| 
						 | 
					@ -80,6 +102,7 @@
 | 
				
			||||||
		"@types/punycode": "2.1.0",
 | 
							"@types/punycode": "2.1.0",
 | 
				
			||||||
		"@types/sanitize-html": "2.9.0",
 | 
							"@types/sanitize-html": "2.9.0",
 | 
				
			||||||
		"@types/seedrandom": "3.0.5",
 | 
							"@types/seedrandom": "3.0.5",
 | 
				
			||||||
 | 
							"@types/testing-library__jest-dom": "^5.14.5",
 | 
				
			||||||
		"@types/throttle-debounce": "5.0.0",
 | 
							"@types/throttle-debounce": "5.0.0",
 | 
				
			||||||
		"@types/tinycolor2": "1.4.3",
 | 
							"@types/tinycolor2": "1.4.3",
 | 
				
			||||||
		"@types/uuid": "9.0.1",
 | 
							"@types/uuid": "9.0.1",
 | 
				
			||||||
| 
						 | 
					@ -89,13 +112,24 @@
 | 
				
			||||||
		"@typescript-eslint/parser": "5.57.0",
 | 
							"@typescript-eslint/parser": "5.57.0",
 | 
				
			||||||
		"@vitest/coverage-c8": "^0.29.8",
 | 
							"@vitest/coverage-c8": "^0.29.8",
 | 
				
			||||||
		"@vue/runtime-core": "3.2.47",
 | 
							"@vue/runtime-core": "3.2.47",
 | 
				
			||||||
 | 
							"astring": "^1.8.4",
 | 
				
			||||||
 | 
							"chokidar-cli": "^3.0.0",
 | 
				
			||||||
 | 
							"chromatic": "^6.17.2",
 | 
				
			||||||
		"cross-env": "7.0.3",
 | 
							"cross-env": "7.0.3",
 | 
				
			||||||
		"cypress": "12.9.0",
 | 
							"cypress": "12.9.0",
 | 
				
			||||||
		"eslint": "8.37.0",
 | 
							"eslint": "8.37.0",
 | 
				
			||||||
		"eslint-plugin-import": "2.27.5",
 | 
							"eslint-plugin-import": "2.27.5",
 | 
				
			||||||
		"eslint-plugin-vue": "9.10.0",
 | 
							"eslint-plugin-vue": "9.10.0",
 | 
				
			||||||
 | 
							"fast-glob": "^3.2.12",
 | 
				
			||||||
		"happy-dom": "8.9.0",
 | 
							"happy-dom": "8.9.0",
 | 
				
			||||||
 | 
							"msw": "^1.1.0",
 | 
				
			||||||
 | 
							"msw-storybook-addon": "^1.8.0",
 | 
				
			||||||
 | 
							"prettier": "^2.8.4",
 | 
				
			||||||
 | 
							"react": "^18.2.0",
 | 
				
			||||||
 | 
							"react-dom": "^18.2.0",
 | 
				
			||||||
		"start-server-and-test": "2.0.0",
 | 
							"start-server-and-test": "2.0.0",
 | 
				
			||||||
 | 
							"storybook": "7.0.0-rc.10",
 | 
				
			||||||
 | 
							"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
 | 
				
			||||||
		"summaly": "github:misskey-dev/summaly",
 | 
							"summaly": "github:misskey-dev/summaly",
 | 
				
			||||||
		"vitest": "^0.29.8",
 | 
							"vitest": "^0.29.8",
 | 
				
			||||||
		"vitest-fetch-mock": "^0.2.2",
 | 
							"vitest-fetch-mock": "^0.2.2",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										303
									
								
								packages/frontend/public/mockServiceWorker.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								packages/frontend/public/mockServiceWorker.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,303 @@
 | 
				
			||||||
 | 
					/* eslint-disable */
 | 
				
			||||||
 | 
					/* tslint:disable */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Mock Service Worker (1.1.0).
 | 
				
			||||||
 | 
					 * @see https://github.com/mswjs/msw
 | 
				
			||||||
 | 
					 * - Please do NOT modify this file.
 | 
				
			||||||
 | 
					 * - Please do NOT serve this file on production.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
 | 
				
			||||||
 | 
					const activeClientIds = new Set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					self.addEventListener('install', function () {
 | 
				
			||||||
 | 
					  self.skipWaiting()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					self.addEventListener('activate', function (event) {
 | 
				
			||||||
 | 
					  event.waitUntil(self.clients.claim())
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					self.addEventListener('message', async function (event) {
 | 
				
			||||||
 | 
					  const clientId = event.source.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!clientId || !self.clients) {
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const client = await self.clients.get(clientId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!client) {
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const allClients = await self.clients.matchAll({
 | 
				
			||||||
 | 
					    type: 'window',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  switch (event.data) {
 | 
				
			||||||
 | 
					    case 'KEEPALIVE_REQUEST': {
 | 
				
			||||||
 | 
					      sendToClient(client, {
 | 
				
			||||||
 | 
					        type: 'KEEPALIVE_RESPONSE',
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      break
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case 'INTEGRITY_CHECK_REQUEST': {
 | 
				
			||||||
 | 
					      sendToClient(client, {
 | 
				
			||||||
 | 
					        type: 'INTEGRITY_CHECK_RESPONSE',
 | 
				
			||||||
 | 
					        payload: INTEGRITY_CHECKSUM,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      break
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case 'MOCK_ACTIVATE': {
 | 
				
			||||||
 | 
					      activeClientIds.add(clientId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      sendToClient(client, {
 | 
				
			||||||
 | 
					        type: 'MOCKING_ENABLED',
 | 
				
			||||||
 | 
					        payload: true,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      break
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case 'MOCK_DEACTIVATE': {
 | 
				
			||||||
 | 
					      activeClientIds.delete(clientId)
 | 
				
			||||||
 | 
					      break
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case 'CLIENT_CLOSED': {
 | 
				
			||||||
 | 
					      activeClientIds.delete(clientId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const remainingClients = allClients.filter((client) => {
 | 
				
			||||||
 | 
					        return client.id !== clientId
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Unregister itself when there are no more clients
 | 
				
			||||||
 | 
					      if (remainingClients.length === 0) {
 | 
				
			||||||
 | 
					        self.registration.unregister()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      break
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					self.addEventListener('fetch', function (event) {
 | 
				
			||||||
 | 
					  const { request } = event
 | 
				
			||||||
 | 
					  const accept = request.headers.get('accept') || ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Bypass server-sent events.
 | 
				
			||||||
 | 
					  if (accept.includes('text/event-stream')) {
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Bypass navigation requests.
 | 
				
			||||||
 | 
					  if (request.mode === 'navigate') {
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Opening the DevTools triggers the "only-if-cached" request
 | 
				
			||||||
 | 
					  // that cannot be handled by the worker. Bypass such requests.
 | 
				
			||||||
 | 
					  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Bypass all requests when there are no active clients.
 | 
				
			||||||
 | 
					  // Prevents the self-unregistered worked from handling requests
 | 
				
			||||||
 | 
					  // after it's been deleted (still remains active until the next reload).
 | 
				
			||||||
 | 
					  if (activeClientIds.size === 0) {
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Generate unique request ID.
 | 
				
			||||||
 | 
					  const requestId = Math.random().toString(16).slice(2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  event.respondWith(
 | 
				
			||||||
 | 
					    handleRequest(event, requestId).catch((error) => {
 | 
				
			||||||
 | 
					      if (error.name === 'NetworkError') {
 | 
				
			||||||
 | 
					        console.warn(
 | 
				
			||||||
 | 
					          '[MSW] Successfully emulated a network error for the "%s %s" request.',
 | 
				
			||||||
 | 
					          request.method,
 | 
				
			||||||
 | 
					          request.url,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // At this point, any exception indicates an issue with the original request/response.
 | 
				
			||||||
 | 
					      console.error(
 | 
				
			||||||
 | 
					        `\
 | 
				
			||||||
 | 
					[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
 | 
				
			||||||
 | 
					        request.method,
 | 
				
			||||||
 | 
					        request.url,
 | 
				
			||||||
 | 
					        `${error.name}: ${error.message}`,
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handleRequest(event, requestId) {
 | 
				
			||||||
 | 
					  const client = await resolveMainClient(event)
 | 
				
			||||||
 | 
					  const response = await getResponse(event, client, requestId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Send back the response clone for the "response:*" life-cycle events.
 | 
				
			||||||
 | 
					  // Ensure MSW is active and ready to handle the message, otherwise
 | 
				
			||||||
 | 
					  // this message will pend indefinitely.
 | 
				
			||||||
 | 
					  if (client && activeClientIds.has(client.id)) {
 | 
				
			||||||
 | 
					    ;(async function () {
 | 
				
			||||||
 | 
					      const clonedResponse = response.clone()
 | 
				
			||||||
 | 
					      sendToClient(client, {
 | 
				
			||||||
 | 
					        type: 'RESPONSE',
 | 
				
			||||||
 | 
					        payload: {
 | 
				
			||||||
 | 
					          requestId,
 | 
				
			||||||
 | 
					          type: clonedResponse.type,
 | 
				
			||||||
 | 
					          ok: clonedResponse.ok,
 | 
				
			||||||
 | 
					          status: clonedResponse.status,
 | 
				
			||||||
 | 
					          statusText: clonedResponse.statusText,
 | 
				
			||||||
 | 
					          body:
 | 
				
			||||||
 | 
					            clonedResponse.body === null ? null : await clonedResponse.text(),
 | 
				
			||||||
 | 
					          headers: Object.fromEntries(clonedResponse.headers.entries()),
 | 
				
			||||||
 | 
					          redirected: clonedResponse.redirected,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    })()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return response
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Resolve the main client for the given event.
 | 
				
			||||||
 | 
					// Client that issues a request doesn't necessarily equal the client
 | 
				
			||||||
 | 
					// that registered the worker. It's with the latter the worker should
 | 
				
			||||||
 | 
					// communicate with during the response resolving phase.
 | 
				
			||||||
 | 
					async function resolveMainClient(event) {
 | 
				
			||||||
 | 
					  const client = await self.clients.get(event.clientId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (client?.frameType === 'top-level') {
 | 
				
			||||||
 | 
					    return client
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const allClients = await self.clients.matchAll({
 | 
				
			||||||
 | 
					    type: 'window',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return allClients
 | 
				
			||||||
 | 
					    .filter((client) => {
 | 
				
			||||||
 | 
					      // Get only those clients that are currently visible.
 | 
				
			||||||
 | 
					      return client.visibilityState === 'visible'
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .find((client) => {
 | 
				
			||||||
 | 
					      // Find the client ID that's recorded in the
 | 
				
			||||||
 | 
					      // set of clients that have registered the worker.
 | 
				
			||||||
 | 
					      return activeClientIds.has(client.id)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getResponse(event, client, requestId) {
 | 
				
			||||||
 | 
					  const { request } = event
 | 
				
			||||||
 | 
					  const clonedRequest = request.clone()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function passthrough() {
 | 
				
			||||||
 | 
					    // Clone the request because it might've been already used
 | 
				
			||||||
 | 
					    // (i.e. its body has been read and sent to the client).
 | 
				
			||||||
 | 
					    const headers = Object.fromEntries(clonedRequest.headers.entries())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove MSW-specific request headers so the bypassed requests
 | 
				
			||||||
 | 
					    // comply with the server's CORS preflight check.
 | 
				
			||||||
 | 
					    // Operate with the headers as an object because request "Headers"
 | 
				
			||||||
 | 
					    // are immutable.
 | 
				
			||||||
 | 
					    delete headers['x-msw-bypass']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return fetch(clonedRequest, { headers })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Bypass mocking when the client is not active.
 | 
				
			||||||
 | 
					  if (!client) {
 | 
				
			||||||
 | 
					    return passthrough()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Bypass initial page load requests (i.e. static assets).
 | 
				
			||||||
 | 
					  // The absence of the immediate/parent client in the map of the active clients
 | 
				
			||||||
 | 
					  // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
 | 
				
			||||||
 | 
					  // and is not ready to handle requests.
 | 
				
			||||||
 | 
					  if (!activeClientIds.has(client.id)) {
 | 
				
			||||||
 | 
					    return passthrough()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Bypass requests with the explicit bypass header.
 | 
				
			||||||
 | 
					  // Such requests can be issued by "ctx.fetch()".
 | 
				
			||||||
 | 
					  if (request.headers.get('x-msw-bypass') === 'true') {
 | 
				
			||||||
 | 
					    return passthrough()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Notify the client that a request has been intercepted.
 | 
				
			||||||
 | 
					  const clientMessage = await sendToClient(client, {
 | 
				
			||||||
 | 
					    type: 'REQUEST',
 | 
				
			||||||
 | 
					    payload: {
 | 
				
			||||||
 | 
					      id: requestId,
 | 
				
			||||||
 | 
					      url: request.url,
 | 
				
			||||||
 | 
					      method: request.method,
 | 
				
			||||||
 | 
					      headers: Object.fromEntries(request.headers.entries()),
 | 
				
			||||||
 | 
					      cache: request.cache,
 | 
				
			||||||
 | 
					      mode: request.mode,
 | 
				
			||||||
 | 
					      credentials: request.credentials,
 | 
				
			||||||
 | 
					      destination: request.destination,
 | 
				
			||||||
 | 
					      integrity: request.integrity,
 | 
				
			||||||
 | 
					      redirect: request.redirect,
 | 
				
			||||||
 | 
					      referrer: request.referrer,
 | 
				
			||||||
 | 
					      referrerPolicy: request.referrerPolicy,
 | 
				
			||||||
 | 
					      body: await request.text(),
 | 
				
			||||||
 | 
					      bodyUsed: request.bodyUsed,
 | 
				
			||||||
 | 
					      keepalive: request.keepalive,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  switch (clientMessage.type) {
 | 
				
			||||||
 | 
					    case 'MOCK_RESPONSE': {
 | 
				
			||||||
 | 
					      return respondWithMock(clientMessage.data)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case 'MOCK_NOT_FOUND': {
 | 
				
			||||||
 | 
					      return passthrough()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case 'NETWORK_ERROR': {
 | 
				
			||||||
 | 
					      const { name, message } = clientMessage.data
 | 
				
			||||||
 | 
					      const networkError = new Error(message)
 | 
				
			||||||
 | 
					      networkError.name = name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Rejecting a "respondWith" promise emulates a network error.
 | 
				
			||||||
 | 
					      throw networkError
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return passthrough()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function sendToClient(client, message) {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    const channel = new MessageChannel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channel.port1.onmessage = (event) => {
 | 
				
			||||||
 | 
					      if (event.data && event.data.error) {
 | 
				
			||||||
 | 
					        return reject(event.data.error)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      resolve(event.data)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    client.postMessage(message, [channel.port2])
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function sleep(timeMs) {
 | 
				
			||||||
 | 
					  return new Promise((resolve) => {
 | 
				
			||||||
 | 
					    setTimeout(resolve, timeMs)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function respondWithMock(response) {
 | 
				
			||||||
 | 
					  await sleep(response.delay)
 | 
				
			||||||
 | 
					  return new Response(response.body, response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MkAnalogClock from './MkAnalogClock.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkAnalogClock,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkAnalogClock v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'fullscreen',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAnalogClock>;
 | 
				
			||||||
							
								
								
									
										30
									
								
								packages/frontend/src/components/MkButton.stories.impl.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								packages/frontend/src/components/MkButton.stories.impl.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					/* eslint-disable import/no-default-export */
 | 
				
			||||||
 | 
					/* eslint-disable import/no-duplicates */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MkButton from './MkButton.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkButton,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkButton v-bind="props">Text</MkButton>',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkButton>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					import MkCaptcha from './MkCaptcha.vue';
 | 
				
			||||||
 | 
					void MkCaptcha;
 | 
				
			||||||
| 
						 | 
					@ -17,8 +17,8 @@ import { onMounted, onBeforeUnmount } from 'vue';
 | 
				
			||||||
import MkMenu from './MkMenu.vue';
 | 
					import MkMenu from './MkMenu.vue';
 | 
				
			||||||
import { MenuItem } from './types/menu.vue';
 | 
					import { MenuItem } from './types/menu.vue';
 | 
				
			||||||
import contains from '@/scripts/contains';
 | 
					import contains from '@/scripts/contains';
 | 
				
			||||||
import * as os from '@/os';
 | 
					 | 
				
			||||||
import { defaultStore } from '@/store';
 | 
					import { defaultStore } from '@/store';
 | 
				
			||||||
 | 
					import * as os from '@/os';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
	items: MenuItem[];
 | 
						items: MenuItem[];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div>
 | 
					<div role="menu">
 | 
				
			||||||
	<div
 | 
						<div
 | 
				
			||||||
		ref="itemsEl" v-hotkey="keymap"
 | 
							ref="itemsEl" v-hotkey="keymap"
 | 
				
			||||||
		class="_popup _shadow"
 | 
							class="_popup _shadow"
 | 
				
			||||||
| 
						 | 
					@ -8,37 +8,37 @@
 | 
				
			||||||
		@contextmenu.self="e => e.preventDefault()"
 | 
							@contextmenu.self="e => e.preventDefault()"
 | 
				
			||||||
	>
 | 
						>
 | 
				
			||||||
		<template v-for="(item, i) in items2">
 | 
							<template v-for="(item, i) in items2">
 | 
				
			||||||
			<div v-if="item === null" :class="$style.divider"></div>
 | 
								<div v-if="item === null" role="separator" :class="$style.divider"></div>
 | 
				
			||||||
			<span v-else-if="item.type === 'label'" :class="[$style.label, $style.item]">
 | 
								<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
 | 
				
			||||||
				<span>{{ item.text }}</span>
 | 
									<span>{{ item.text }}</span>
 | 
				
			||||||
			</span>
 | 
								</span>
 | 
				
			||||||
			<span v-else-if="item.type === 'pending'" :tabindex="i" :class="[$style.pending, $style.item]">
 | 
								<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
 | 
				
			||||||
				<span><MkEllipsis/></span>
 | 
									<span><MkEllipsis/></span>
 | 
				
			||||||
			</span>
 | 
								</span>
 | 
				
			||||||
			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
								<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
				
			||||||
				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
									<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
				
			||||||
				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 | 
									<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 | 
				
			||||||
				<span>{{ item.text }}</span>
 | 
									<span>{{ item.text }}</span>
 | 
				
			||||||
				<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 | 
									<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 | 
				
			||||||
			</MkA>
 | 
								</MkA>
 | 
				
			||||||
			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
								<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
				
			||||||
				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
									<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
				
			||||||
				<span>{{ item.text }}</span>
 | 
									<span>{{ item.text }}</span>
 | 
				
			||||||
				<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 | 
									<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 | 
				
			||||||
			</a>
 | 
								</a>
 | 
				
			||||||
			<button v-else-if="item.type === 'user'" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
								<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
				
			||||||
				<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
 | 
									<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
 | 
				
			||||||
				<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 | 
									<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
			<span v-else-if="item.type === 'switch'" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
								<span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
				
			||||||
				<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
 | 
									<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
 | 
				
			||||||
			</span>
 | 
								</span>
 | 
				
			||||||
			<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
 | 
								<button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
 | 
				
			||||||
				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
									<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
				
			||||||
				<span>{{ item.text }}</span>
 | 
									<span>{{ item.text }}</span>
 | 
				
			||||||
				<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
 | 
									<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
			<button v-else :tabindex="i" class="_button" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
								<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
				
			||||||
				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
									<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
				
			||||||
				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 | 
									<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 | 
				
			||||||
				<span>{{ item.text }}</span>
 | 
									<span>{{ item.text }}</span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -150,7 +150,7 @@ function adjustTweetHeight(message: any) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const openPlayer = (): void => {
 | 
					const openPlayer = (): void => {
 | 
				
			||||||
	os.popup(defineAsyncComponent(() => import('@/components/MkYoutubePlayer.vue')), {
 | 
						os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
 | 
				
			||||||
		url: requestUrl.href,
 | 
							url: requestUrl.href,
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										47
									
								
								packages/frontend/src/components/global/MkA.stories.impl.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								packages/frontend/src/components/global/MkA.stories.impl.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { expect } from '@storybook/jest';
 | 
				
			||||||
 | 
					import { userEvent, within } from '@storybook/testing-library';
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MkA from './MkA.vue';
 | 
				
			||||||
 | 
					import { tick } from '@/scripts/test-utils';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkA,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkA v-bind="props">Text</MkA>',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							const canvas = within(canvasElement);
 | 
				
			||||||
 | 
							const a = canvas.getByRole<HTMLAnchorElement>('link');
 | 
				
			||||||
 | 
							await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
 | 
				
			||||||
 | 
							await userEvent.click(a, { button: 2 });
 | 
				
			||||||
 | 
							await tick();
 | 
				
			||||||
 | 
							const menu = canvas.getByRole('menu');
 | 
				
			||||||
 | 
							await expect(menu).toBeInTheDocument();
 | 
				
			||||||
 | 
							await userEvent.click(a, { button: 0 });
 | 
				
			||||||
 | 
							a.blur();
 | 
				
			||||||
 | 
							await tick();
 | 
				
			||||||
 | 
							await expect(menu).not.toBeInTheDocument();
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							to: '#test',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkA>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import { userDetailed } from '../../../.storybook/fakes';
 | 
				
			||||||
 | 
					import MkAcct from './MkAcct.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkAcct,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkAcct v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							user: {
 | 
				
			||||||
 | 
								...userDetailed,
 | 
				
			||||||
 | 
								host: null,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAcct>;
 | 
				
			||||||
 | 
					export const Detail = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							user: userDetailed,
 | 
				
			||||||
 | 
							detail: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAcct>;
 | 
				
			||||||
| 
						 | 
					@ -18,4 +18,3 @@ defineProps<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const host = toUnicode(hostRaw);
 | 
					const host = toUnicode(hostRaw);
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										120
									
								
								packages/frontend/src/components/global/MkAd.stories.impl.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								packages/frontend/src/components/global/MkAd.stories.impl.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,120 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { expect } from '@storybook/jest';
 | 
				
			||||||
 | 
					import { userEvent, within } from '@storybook/testing-library';
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					import MkAd from './MkAd.vue';
 | 
				
			||||||
 | 
					const common = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkAd,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkAd v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							const canvas = within(canvasElement);
 | 
				
			||||||
 | 
							const a = canvas.getByRole<HTMLAnchorElement>('link');
 | 
				
			||||||
 | 
							await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
 | 
				
			||||||
 | 
							const img = within(a).getByRole('img');
 | 
				
			||||||
 | 
							await expect(img).toBeInTheDocument();
 | 
				
			||||||
 | 
							let buttons = canvas.getAllByRole<HTMLButtonElement>('button');
 | 
				
			||||||
 | 
							await expect(buttons).toHaveLength(1);
 | 
				
			||||||
 | 
							const i = buttons[0];
 | 
				
			||||||
 | 
							await expect(i).toBeInTheDocument();
 | 
				
			||||||
 | 
							await userEvent.click(i);
 | 
				
			||||||
 | 
							await expect(a).not.toBeInTheDocument();
 | 
				
			||||||
 | 
							await expect(i).not.toBeInTheDocument();
 | 
				
			||||||
 | 
							buttons = canvas.getAllByRole<HTMLButtonElement>('button');
 | 
				
			||||||
 | 
							await expect(buttons).toHaveLength(args.__hasReduce ? 2 : 1);
 | 
				
			||||||
 | 
							const reduce = args.__hasReduce ? buttons[0] : null;
 | 
				
			||||||
 | 
							const back = buttons[args.__hasReduce ? 1 : 0];
 | 
				
			||||||
 | 
							if (reduce) {
 | 
				
			||||||
 | 
								await expect(reduce).toBeInTheDocument();
 | 
				
			||||||
 | 
								await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							await expect(back).toBeInTheDocument();
 | 
				
			||||||
 | 
							await expect(back).toHaveTextContent(i18n.ts._ad.back);
 | 
				
			||||||
 | 
							await userEvent.click(back);
 | 
				
			||||||
 | 
							if (reduce) {
 | 
				
			||||||
 | 
								await expect(reduce).not.toBeInTheDocument();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							await expect(back).not.toBeInTheDocument();
 | 
				
			||||||
 | 
							const aAgain = canvas.getByRole<HTMLAnchorElement>('link');
 | 
				
			||||||
 | 
							await expect(aAgain).toBeInTheDocument();
 | 
				
			||||||
 | 
							const imgAgain = within(aAgain).getByRole('img');
 | 
				
			||||||
 | 
							await expect(imgAgain).toBeInTheDocument();
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							prefer: [],
 | 
				
			||||||
 | 
							specify: {
 | 
				
			||||||
 | 
								id: 'someadid',
 | 
				
			||||||
 | 
								radio: 1,
 | 
				
			||||||
 | 
								url: '#test',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							__hasReduce: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAd>;
 | 
				
			||||||
 | 
					export const Square = {
 | 
				
			||||||
 | 
						...common,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...common.args,
 | 
				
			||||||
 | 
							specify: {
 | 
				
			||||||
 | 
								...common.args.specify,
 | 
				
			||||||
 | 
								place: 'square',
 | 
				
			||||||
 | 
								imageUrl:
 | 
				
			||||||
 | 
									'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAd>;
 | 
				
			||||||
 | 
					export const Horizontal = {
 | 
				
			||||||
 | 
						...common,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...common.args,
 | 
				
			||||||
 | 
							specify: {
 | 
				
			||||||
 | 
								...common.args.specify,
 | 
				
			||||||
 | 
								place: 'horizontal',
 | 
				
			||||||
 | 
								imageUrl:
 | 
				
			||||||
 | 
									'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAd>;
 | 
				
			||||||
 | 
					export const HorizontalBig = {
 | 
				
			||||||
 | 
						...common,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...common.args,
 | 
				
			||||||
 | 
							specify: {
 | 
				
			||||||
 | 
								...common.args.specify,
 | 
				
			||||||
 | 
								place: 'horizontal-big',
 | 
				
			||||||
 | 
								imageUrl:
 | 
				
			||||||
 | 
									'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAd>;
 | 
				
			||||||
 | 
					export const ZeroRatio = {
 | 
				
			||||||
 | 
						...Square,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Square.args,
 | 
				
			||||||
 | 
							specify: {
 | 
				
			||||||
 | 
								...Square.args.specify,
 | 
				
			||||||
 | 
								ratio: 0,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							__hasReduce: false,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAd>;
 | 
				
			||||||
| 
						 | 
					@ -20,13 +20,13 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { ref } from 'vue';
 | 
					import { ref } from 'vue';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
import { instance } from '@/instance';
 | 
					import { instance } from '@/instance';
 | 
				
			||||||
import { host } from '@/config';
 | 
					import { host } from '@/config';
 | 
				
			||||||
import MkButton from '@/components/MkButton.vue';
 | 
					import MkButton from '@/components/MkButton.vue';
 | 
				
			||||||
import { defaultStore } from '@/store';
 | 
					import { defaultStore } from '@/store';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import { $i } from '@/account';
 | 
					import { $i } from '@/account';
 | 
				
			||||||
import { i18n } from '@/i18n';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Ad = (typeof instance)['ads'][number];
 | 
					type Ad = (typeof instance)['ads'][number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,66 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import { userDetailed } from '../../../.storybook/fakes';
 | 
				
			||||||
 | 
					import MkAvatar from './MkAvatar.vue';
 | 
				
			||||||
 | 
					const common = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkAvatar,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkAvatar v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							user: userDetailed,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						decorators: [
 | 
				
			||||||
 | 
							(Story, context) => ({
 | 
				
			||||||
 | 
								// eslint-disable-next-line quotes
 | 
				
			||||||
 | 
								template: `<div :style="{ display: 'grid', width: '${context.args.size}px', height: '${context.args.size}px' }"><story/></div>`,
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAvatar>;
 | 
				
			||||||
 | 
					export const ProfilePage = {
 | 
				
			||||||
 | 
						...common,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...common.args,
 | 
				
			||||||
 | 
							size: 120,
 | 
				
			||||||
 | 
							indicator: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAvatar>;
 | 
				
			||||||
 | 
					export const ProfilePageCat = {
 | 
				
			||||||
 | 
						...ProfilePage,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...ProfilePage.args,
 | 
				
			||||||
 | 
							user: {
 | 
				
			||||||
 | 
								...userDetailed,
 | 
				
			||||||
 | 
								isCat: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							...ProfilePage.parameters,
 | 
				
			||||||
 | 
							chromatic: {
 | 
				
			||||||
 | 
								/* Your story couldn’t be captured because it exceeds our 25,000,000px limit. Its dimensions are 5,504,893x5,504,892px. Possible ways to resolve:
 | 
				
			||||||
 | 
								 * * Separate pages into components
 | 
				
			||||||
 | 
								 * * Minimize the number of very large elements in a story
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								disableSnapshot: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkAvatar>;
 | 
				
			||||||
| 
						 | 
					@ -148,6 +148,7 @@ watch(() => props.user.avatarBlurhash, () => {
 | 
				
			||||||
		width: 100%;
 | 
							width: 100%;
 | 
				
			||||||
		height: 100%;
 | 
							height: 100%;
 | 
				
			||||||
		padding: 50%;
 | 
							padding: 50%;
 | 
				
			||||||
 | 
							pointer-events: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		&.mask {
 | 
							&.mask {
 | 
				
			||||||
			-webkit-mask:
 | 
								-webkit-mask:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MkCustomEmoji from './MkCustomEmoji.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkCustomEmoji,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkCustomEmoji v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							name: 'mi',
 | 
				
			||||||
 | 
							url: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkCustomEmoji>;
 | 
				
			||||||
 | 
					export const Normal = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							normal: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkCustomEmoji>;
 | 
				
			||||||
 | 
					export const Missing = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							name: Default.args.name,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkCustomEmoji>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import isChromatic from 'chromatic/isChromatic';
 | 
				
			||||||
 | 
					import MkEllipsis from './MkEllipsis.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkEllipsis,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkEllipsis v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							static: isChromatic(),
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkEllipsis>;
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,19 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<span :class="$style.root">
 | 
					<span :class="[$style.root, { [$style.static]: static }]">
 | 
				
			||||||
	<span :class="$style.dot">.</span><span :class="$style.dot">.</span><span :class="$style.dot">.</span>
 | 
						<span :class="$style.dot">.</span><span :class="$style.dot">.</span><span :class="$style.dot">.</span>
 | 
				
			||||||
</span>
 | 
					</span>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
 | 
						static?: boolean;
 | 
				
			||||||
 | 
					}>(), {
 | 
				
			||||||
 | 
						static: false,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" module>
 | 
					<style lang="scss" module>
 | 
				
			||||||
@keyframes ellipsis {
 | 
					@keyframes ellipsis {
 | 
				
			||||||
	0%, 80%, 100% {
 | 
						0%, 80%, 100% {
 | 
				
			||||||
| 
						 | 
					@ -15,7 +25,9 @@
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.root {
 | 
					.root {
 | 
				
			||||||
	
 | 
						&.static > .dot {
 | 
				
			||||||
 | 
							animation-play-state: paused;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dot {
 | 
					.dot {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MkEmoji from './MkEmoji.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkEmoji,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkEmoji v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							emoji: '❤',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkEmoji>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					export const argTypes = {
 | 
				
			||||||
 | 
						retry: {
 | 
				
			||||||
 | 
							action: 'retry',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,60 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import isChromatic from 'chromatic/isChromatic';
 | 
				
			||||||
 | 
					import MkLoading from './MkLoading.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkLoading,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkLoading v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							static: isChromatic(),
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkLoading>;
 | 
				
			||||||
 | 
					export const Inline = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							inline: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkLoading>;
 | 
				
			||||||
 | 
					export const Colored = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							colored: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkLoading>;
 | 
				
			||||||
 | 
					export const Mini = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							mini: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkLoading>;
 | 
				
			||||||
 | 
					export const Em = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							em: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkLoading>;
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@
 | 
				
			||||||
				<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
 | 
									<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
 | 
				
			||||||
			</g>
 | 
								</g>
 | 
				
			||||||
		</svg>
 | 
							</svg>
 | 
				
			||||||
		<svg :class="[$style.spinner, $style.fg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
 | 
							<svg :class="[$style.spinner, $style.fg, { [$style.static]: static }]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
			<g transform="matrix(1.125,0,0,1.125,12,12)">
 | 
								<g transform="matrix(1.125,0,0,1.125,12,12)">
 | 
				
			||||||
				<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
 | 
									<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
 | 
				
			||||||
			</g>
 | 
								</g>
 | 
				
			||||||
| 
						 | 
					@ -19,11 +19,13 @@
 | 
				
			||||||
import { } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = withDefaults(defineProps<{
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
 | 
						static?: boolean;
 | 
				
			||||||
	inline?: boolean;
 | 
						inline?: boolean;
 | 
				
			||||||
	colored?: boolean;
 | 
						colored?: boolean;
 | 
				
			||||||
	mini?: boolean;
 | 
						mini?: boolean;
 | 
				
			||||||
	em?: boolean;
 | 
						em?: boolean;
 | 
				
			||||||
}>(), {
 | 
					}>(), {
 | 
				
			||||||
 | 
						static: false,
 | 
				
			||||||
	inline: false,
 | 
						inline: false,
 | 
				
			||||||
	colored: true,
 | 
						colored: true,
 | 
				
			||||||
	mini: false,
 | 
						mini: false,
 | 
				
			||||||
| 
						 | 
					@ -97,5 +99,9 @@ const props = withDefaults(defineProps<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.fg {
 | 
					.fg {
 | 
				
			||||||
	animation: spinner 0.5s linear infinite;
 | 
						animation: spinner 0.5s linear infinite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.static {
 | 
				
			||||||
 | 
							animation-play-state: paused;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,74 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.vue';
 | 
				
			||||||
 | 
					import { within } from '@storybook/testing-library';
 | 
				
			||||||
 | 
					import { expect } from '@storybook/jest';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkMisskeyFlavoredMarkdown,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkMisskeyFlavoredMarkdown v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							const canvas = within(canvasElement);
 | 
				
			||||||
 | 
							if (args.plain) {
 | 
				
			||||||
 | 
								const aiHelloMiskist = canvas.getByText('@ai *Hello*, #Miskist!');
 | 
				
			||||||
 | 
								await expect(aiHelloMiskist).toBeInTheDocument();
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								const ai = canvas.getByText('@ai');
 | 
				
			||||||
 | 
								await expect(ai).toBeInTheDocument();
 | 
				
			||||||
 | 
								await expect(ai.closest('a')).toHaveAttribute('href', '/@ai');
 | 
				
			||||||
 | 
								const hello = canvas.getByText('Hello');
 | 
				
			||||||
 | 
								await expect(hello).toBeInTheDocument();
 | 
				
			||||||
 | 
								await expect(hello.style.fontStyle).toBe('oblique');
 | 
				
			||||||
 | 
								const miskist = canvas.getByText('#Miskist');
 | 
				
			||||||
 | 
								await expect(miskist).toBeInTheDocument();
 | 
				
			||||||
 | 
								await expect(miskist).toHaveAttribute('href', args.isNote ?? true ? '/tags/Miskist' : '/user-tags/Miskist');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const heart = canvas.getByAltText('❤');
 | 
				
			||||||
 | 
							await expect(heart).toBeInTheDocument();
 | 
				
			||||||
 | 
							await expect(heart).toHaveAttribute('src', '/twemoji/2764.svg');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							text: '@ai *Hello*, #Miskist! ❤',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
 | 
				
			||||||
 | 
					export const Plain = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							plain: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
 | 
				
			||||||
 | 
					export const Nowrap = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							nowrap: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
 | 
				
			||||||
 | 
					export const IsNotNote = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							isNote: false,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,98 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MkPageHeader from './MkPageHeader.vue';
 | 
				
			||||||
 | 
					export const Empty = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkPageHeader,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkPageHeader v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							static: true,
 | 
				
			||||||
 | 
							tabs: [],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
							chromatic: {
 | 
				
			||||||
 | 
								/* This component has animations that are implemented with JavaScript. So it's unstable to take a snapshot. */
 | 
				
			||||||
 | 
								disableSnapshot: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkPageHeader>;
 | 
				
			||||||
 | 
					export const OneTab = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							tab: 'sometabkey',
 | 
				
			||||||
 | 
							tabs: [
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									key: 'sometabkey',
 | 
				
			||||||
 | 
									title: 'Some Tab Title',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkPageHeader>;
 | 
				
			||||||
 | 
					export const Icon = {
 | 
				
			||||||
 | 
						...OneTab,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...OneTab.args,
 | 
				
			||||||
 | 
							tabs: [
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									...OneTab.args.tabs[0],
 | 
				
			||||||
 | 
									icon: 'ti ti-home',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkPageHeader>;
 | 
				
			||||||
 | 
					export const IconOnly = {
 | 
				
			||||||
 | 
						...Icon,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Icon.args,
 | 
				
			||||||
 | 
							tabs: [
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									...Icon.args.tabs[0],
 | 
				
			||||||
 | 
									title: undefined,
 | 
				
			||||||
 | 
									iconOnly: true,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkPageHeader>;
 | 
				
			||||||
 | 
					export const SomeTabs = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							tab: 'princess',
 | 
				
			||||||
 | 
							tabs: [
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									key: 'princess',
 | 
				
			||||||
 | 
									title: 'Princess',
 | 
				
			||||||
 | 
									icon: 'ti ti-crown',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									key: 'fairy',
 | 
				
			||||||
 | 
									title: 'Fairy',
 | 
				
			||||||
 | 
									icon: 'ti ti-snowflake',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									key: 'angel',
 | 
				
			||||||
 | 
									title: 'Angel',
 | 
				
			||||||
 | 
									icon: 'ti ti-feather',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkPageHeader>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import MkPageHeader_tabs from './MkPageHeader.tabs.vue';
 | 
				
			||||||
 | 
					void MkPageHeader_tabs;
 | 
				
			||||||
| 
						 | 
					@ -33,14 +33,18 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
export type Tab = {
 | 
					export type Tab = {
 | 
				
			||||||
	key: string;
 | 
						key: string;
 | 
				
			||||||
	title: string;
 | 
					 | 
				
			||||||
	icon?: string;
 | 
					 | 
				
			||||||
	iconOnly?: boolean;
 | 
					 | 
				
			||||||
	onClick?: (ev: MouseEvent) => void;
 | 
						onClick?: (ev: MouseEvent) => void;
 | 
				
			||||||
} & {
 | 
					} & (
 | 
				
			||||||
	iconOnly: true;
 | 
						| {
 | 
				
			||||||
	iccn: string;
 | 
								iconOnly?: false;
 | 
				
			||||||
};
 | 
								title: string;
 | 
				
			||||||
 | 
								icon?: string;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						| {
 | 
				
			||||||
 | 
								iconOnly: true;
 | 
				
			||||||
 | 
								icon: string;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import MkStickyContainer from './MkStickyContainer.vue';
 | 
				
			||||||
 | 
					void MkStickyContainer;
 | 
				
			||||||
							
								
								
									
										312
									
								
								packages/frontend/src/components/global/MkTime.stories.impl.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								packages/frontend/src/components/global/MkTime.stories.impl.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,312 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { expect } from '@storybook/jest';
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import MkTime from './MkTime.vue';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					import { dateTimeFormat } from '@/scripts/intl-const';
 | 
				
			||||||
 | 
					const now = new Date('2023-04-01T00:00:00.000Z');
 | 
				
			||||||
 | 
					const future = new Date(8640000000000000);
 | 
				
			||||||
 | 
					const oneHourAgo = new Date(now.getTime() - 3600000);
 | 
				
			||||||
 | 
					const oneDayAgo = new Date(now.getTime() - 86400000);
 | 
				
			||||||
 | 
					const oneWeekAgo = new Date(now.getTime() - 604800000);
 | 
				
			||||||
 | 
					const oneMonthAgo = new Date(now.getTime() - 2592000000);
 | 
				
			||||||
 | 
					const oneYearAgo = new Date(now.getTime() - 31536000000);
 | 
				
			||||||
 | 
					export const Empty = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkTime,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkTime v-bind="props" />',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(i18n.ts._ago.invalid);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const RelativeFuture = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(i18n.ts._ago.future);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: future,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const AbsoluteFuture = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: future,
 | 
				
			||||||
 | 
							mode: 'absolute',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const DetailFuture = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play(context) {
 | 
				
			||||||
 | 
							await AbsoluteFuture.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(' (');
 | 
				
			||||||
 | 
							await RelativeFuture.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(')');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: future,
 | 
				
			||||||
 | 
							mode: 'detail',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const RelativeNow = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(i18n.ts._ago.justNow);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: now,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'relative',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const AbsoluteNow = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: now,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'absolute',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const DetailNow = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play(context) {
 | 
				
			||||||
 | 
							await AbsoluteNow.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(' (');
 | 
				
			||||||
 | 
							await RelativeNow.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(')');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: now,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'detail',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const RelativeOneHourAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 }));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneHourAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'relative',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const AbsoluteOneHourAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneHourAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'absolute',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const DetailOneHourAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play(context) {
 | 
				
			||||||
 | 
							await AbsoluteOneHourAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(' (');
 | 
				
			||||||
 | 
							await RelativeOneHourAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(')');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneHourAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'detail',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const RelativeOneDayAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 }));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneDayAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'relative',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const AbsoluteOneDayAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneDayAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'absolute',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const DetailOneDayAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play(context) {
 | 
				
			||||||
 | 
							await AbsoluteOneDayAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(' (');
 | 
				
			||||||
 | 
							await RelativeOneDayAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(')');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneDayAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'detail',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const RelativeOneWeekAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 }));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneWeekAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'relative',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const AbsoluteOneWeekAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneWeekAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'absolute',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const DetailOneWeekAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play(context) {
 | 
				
			||||||
 | 
							await AbsoluteOneWeekAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(' (');
 | 
				
			||||||
 | 
							await RelativeOneWeekAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(')');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneWeekAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'detail',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const RelativeOneMonthAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 }));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneMonthAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'relative',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const AbsoluteOneMonthAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneMonthAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'absolute',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const DetailOneMonthAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play(context) {
 | 
				
			||||||
 | 
							await AbsoluteOneMonthAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(' (');
 | 
				
			||||||
 | 
							await RelativeOneMonthAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(')');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneMonthAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'detail',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const RelativeOneYearAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 }));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneYearAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'relative',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const AbsoluteOneYearAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play({ canvasElement, args }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneYearAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'absolute',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
 | 
					export const DetailOneYearAgo = {
 | 
				
			||||||
 | 
						...Empty,
 | 
				
			||||||
 | 
						async play(context) {
 | 
				
			||||||
 | 
							await AbsoluteOneYearAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(' (');
 | 
				
			||||||
 | 
							await RelativeOneYearAgo.play(context);
 | 
				
			||||||
 | 
							await expect(context.canvasElement).toHaveTextContent(')');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Empty.args,
 | 
				
			||||||
 | 
							time: oneYearAgo,
 | 
				
			||||||
 | 
							origin: now,
 | 
				
			||||||
 | 
							mode: 'detail',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkTime>;
 | 
				
			||||||
| 
						 | 
					@ -14,8 +14,10 @@ import { dateTimeFormat } from '@/scripts/intl-const';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = withDefaults(defineProps<{
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	time: Date | string | number | null;
 | 
						time: Date | string | number | null;
 | 
				
			||||||
 | 
						origin?: Date | null;
 | 
				
			||||||
	mode?: 'relative' | 'absolute' | 'detail';
 | 
						mode?: 'relative' | 'absolute' | 'detail';
 | 
				
			||||||
}>(), {
 | 
					}>(), {
 | 
				
			||||||
 | 
						origin: null,
 | 
				
			||||||
	mode: 'relative',
 | 
						mode: 'relative',
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +27,7 @@ const _time = props.time == null ? NaN :
 | 
				
			||||||
const invalid = Number.isNaN(_time);
 | 
					const invalid = Number.isNaN(_time);
 | 
				
			||||||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
 | 
					const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let now = $ref((new Date()).getTime());
 | 
					let now = $ref((props.origin ?? new Date()).getTime());
 | 
				
			||||||
const relative = $computed<string>(() => {
 | 
					const relative = $computed<string>(() => {
 | 
				
			||||||
	if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
 | 
						if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
 | 
				
			||||||
	if (invalid) return i18n.ts._ago.invalid;
 | 
						if (invalid) return i18n.ts._ago.invalid;
 | 
				
			||||||
| 
						 | 
					@ -46,7 +48,7 @@ const relative = $computed<string>(() => {
 | 
				
			||||||
let tickId: number;
 | 
					let tickId: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function tick() {
 | 
					function tick() {
 | 
				
			||||||
	now = (new Date()).getTime();
 | 
						now = props.origin ?? (new Date()).getTime();
 | 
				
			||||||
	const ago = (now - _time) / 1000/*ms*/;
 | 
						const ago = (now - _time) / 1000/*ms*/;
 | 
				
			||||||
	const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
 | 
						const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,77 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { expect } from '@storybook/jest';
 | 
				
			||||||
 | 
					import { userEvent, within } from '@storybook/testing-library';
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import { rest } from 'msw';
 | 
				
			||||||
 | 
					import { commonHandlers } from '../../../.storybook/mocks';
 | 
				
			||||||
 | 
					import MkUrl from './MkUrl.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkUrl,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkUrl v-bind="props">Text</MkUrl>',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							const canvas = within(canvasElement);
 | 
				
			||||||
 | 
							const a = canvas.getByRole<HTMLAnchorElement>('link');
 | 
				
			||||||
 | 
							await expect(a).toHaveAttribute('href', 'https://misskey-hub.net/');
 | 
				
			||||||
 | 
							await userEvent.hover(a);
 | 
				
			||||||
 | 
							/*
 | 
				
			||||||
 | 
							await tick(); // FIXME: wait for network request
 | 
				
			||||||
 | 
							const anchors = canvas.getAllByRole<HTMLAnchorElement>('link');
 | 
				
			||||||
 | 
							const popup = anchors.find(anchor => anchor !== a)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
							await expect(popup).toBeInTheDocument();
 | 
				
			||||||
 | 
							await expect(popup).toHaveAttribute('href', 'https://misskey-hub.net/');
 | 
				
			||||||
 | 
							await expect(popup).toHaveTextContent('Misskey Hub');
 | 
				
			||||||
 | 
							await expect(popup).toHaveTextContent('Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。');
 | 
				
			||||||
 | 
							await expect(popup).toHaveTextContent('misskey-hub.net');
 | 
				
			||||||
 | 
							const icon = within(popup).getByRole('img');
 | 
				
			||||||
 | 
							await expect(icon).toBeInTheDocument();
 | 
				
			||||||
 | 
							await expect(icon).toHaveAttribute('src', 'https://misskey-hub.net/favicon.ico');
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							await userEvent.unhover(a);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							url: 'https://misskey-hub.net/',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
							msw: {
 | 
				
			||||||
 | 
								handlers: [
 | 
				
			||||||
 | 
									...commonHandlers,
 | 
				
			||||||
 | 
									rest.get('/url', (req, res, ctx) => {
 | 
				
			||||||
 | 
										return res(ctx.json({
 | 
				
			||||||
 | 
											title: 'Misskey Hub',
 | 
				
			||||||
 | 
											icon: 'https://misskey-hub.net/favicon.ico',
 | 
				
			||||||
 | 
											description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
 | 
				
			||||||
 | 
											thumbnail: null,
 | 
				
			||||||
 | 
											player: {
 | 
				
			||||||
 | 
												url: null,
 | 
				
			||||||
 | 
												width: null,
 | 
				
			||||||
 | 
												height: null,
 | 
				
			||||||
 | 
												allow: [],
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
											sitename: 'misskey-hub.net',
 | 
				
			||||||
 | 
											sensitive: false,
 | 
				
			||||||
 | 
											url: 'https://misskey-hub.net/',
 | 
				
			||||||
 | 
										}));
 | 
				
			||||||
 | 
									}),
 | 
				
			||||||
 | 
								],
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkUrl>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import { expect } from '@storybook/jest';
 | 
				
			||||||
 | 
					import { userEvent, within } from '@storybook/testing-library';
 | 
				
			||||||
 | 
					import { StoryObj } from '@storybook/vue3';
 | 
				
			||||||
 | 
					import { userDetailed } from '../../../.storybook/fakes';
 | 
				
			||||||
 | 
					import MkUserName from './MkUserName.vue';
 | 
				
			||||||
 | 
					export const Default = {
 | 
				
			||||||
 | 
						render(args) {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								components: {
 | 
				
			||||||
 | 
									MkUserName,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setup() {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										args,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								computed: {
 | 
				
			||||||
 | 
									props() {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											...this.args,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								template: '<MkUserName v-bind="props"/>',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(userDetailed.name);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							user: userDetailed,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						parameters: {
 | 
				
			||||||
 | 
							layout: 'centered',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkUserName>;
 | 
				
			||||||
 | 
					export const Anonymous = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						async play({ canvasElement }) {
 | 
				
			||||||
 | 
							await expect(canvasElement).toHaveTextContent(userDetailed.username);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							user: {
 | 
				
			||||||
 | 
								...userDetailed,
 | 
				
			||||||
 | 
								name: null,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkUserName>;
 | 
				
			||||||
 | 
					export const Wrap = {
 | 
				
			||||||
 | 
						...Default,
 | 
				
			||||||
 | 
						args: {
 | 
				
			||||||
 | 
							...Default.args,
 | 
				
			||||||
 | 
							nowrap: false,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} satisfies StoryObj<typeof MkUserName>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					import RouterView from './RouterView.vue';
 | 
				
			||||||
 | 
					void RouterView;
 | 
				
			||||||
							
								
								
									
										12
									
								
								packages/frontend/src/index.mdx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/frontend/src/index.mdx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { Meta } from '@storybook/blocks'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<Meta title="index" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Welcome to Misskey Storybook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This project uses [Storybook](https://storybook.js.org/) to develop and document components.
 | 
				
			||||||
 | 
					You can find more information about the usage of Storybook in this project in the CONTRIBUTING.md file placed in the root of this repository.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Misskey Storybook is under development and not all components are documented yet.
 | 
				
			||||||
 | 
					Contributions are welcome! Please refer to [#10336](https://github.com/misskey-dev/misskey/issues/10336) for more information.
 | 
				
			||||||
 | 
					Thank you for your support!
 | 
				
			||||||
| 
						 | 
					@ -77,7 +77,10 @@ async function renderChart() {
 | 
				
			||||||
			barPercentage: 0.7,
 | 
								barPercentage: 0.7,
 | 
				
			||||||
			categoryPercentage: 0.7,
 | 
								categoryPercentage: 0.7,
 | 
				
			||||||
			fill: true,
 | 
								fill: true,
 | 
				
			||||||
		} satisfies ChartDataset, extra);
 | 
							/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
 | 
				
			||||||
 | 
							} satisfies ChartData, extra);
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							}, extra);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	chartInstance = new Chart(chartEl, {
 | 
						chartInstance = new Chart(chartEl, {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -113,6 +113,9 @@ async function renderChart() {
 | 
				
			||||||
					const a = c.chart.chartArea ?? {};
 | 
										const a = c.chart.chartArea ?? {};
 | 
				
			||||||
					return (a.bottom - a.top) / 7 - marginEachCell;
 | 
										return (a.bottom - a.top) / 7 - marginEachCell;
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
 | 
								/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
 | 
				
			||||||
 | 
								}] satisfies ChartData[],
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
			}],
 | 
								}],
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		options: {
 | 
							options: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,7 +76,10 @@ async function renderChart() {
 | 
				
			||||||
			borderRadius: 4,
 | 
								borderRadius: 4,
 | 
				
			||||||
			barPercentage: 0.9,
 | 
								barPercentage: 0.9,
 | 
				
			||||||
			fill: true,
 | 
								fill: true,
 | 
				
			||||||
		} satisfies ChartDataset, extra);
 | 
							/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
 | 
				
			||||||
 | 
							} satisfies ChartData, extra);
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							}, extra);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	chartInstance = new Chart(chartEl, {
 | 
						chartInstance = new Chart(chartEl, {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -77,7 +77,10 @@ async function renderChart() {
 | 
				
			||||||
			barPercentage: 0.7,
 | 
								barPercentage: 0.7,
 | 
				
			||||||
			categoryPercentage: 0.7,
 | 
								categoryPercentage: 0.7,
 | 
				
			||||||
			fill: true,
 | 
								fill: true,
 | 
				
			||||||
		} satisfies ChartDataset, extra);
 | 
							/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
 | 
				
			||||||
 | 
							} satisfies ChartData, extra);
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							}, extra);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	chartInstance = new Chart(chartEl, {
 | 
						chartInstance = new Chart(chartEl, {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -443,11 +443,14 @@ export const ACHIEVEMENT_BADGES = {
 | 
				
			||||||
		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
 | 
							bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
 | 
				
			||||||
		frame: 'bronze',
 | 
							frame: 'bronze',
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
 | 
				
			||||||
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
 | 
					} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
 | 
				
			||||||
	img: string;
 | 
						img: string;
 | 
				
			||||||
	bg: string | null;
 | 
						bg: string | null;
 | 
				
			||||||
	frame: 'bronze' | 'silver' | 'gold' | 'platinum';
 | 
						frame: 'bronze' | 'silver' | 'gold' | 'platinum';
 | 
				
			||||||
}>;
 | 
					}>;
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const claimedAchievements: typeof ACHIEVEMENT_TYPES[number][] = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : [];
 | 
					export const claimedAchievements: typeof ACHIEVEMENT_TYPES[number][] = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								packages/frontend/src/scripts/test-utils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packages/frontend/src/scripts/test-utils.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					/// <reference types="@testing-library/jest-dom"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function tick(): Promise<void> {
 | 
				
			||||||
 | 
						// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
				
			||||||
 | 
						await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -43,5 +43,8 @@
 | 
				
			||||||
		".eslintrc.js",
 | 
							".eslintrc.js",
 | 
				
			||||||
		"./**/*.ts",
 | 
							"./**/*.ts",
 | 
				
			||||||
		"./**/*.vue"
 | 
							"./**/*.vue"
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
						"exclude": [
 | 
				
			||||||
 | 
							".storybook/**/*",
 | 
				
			||||||
	]
 | 
						]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,6 @@
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
import pluginVue from '@vitejs/plugin-vue';
 | 
					import pluginVue from '@vitejs/plugin-vue';
 | 
				
			||||||
import { defineConfig } from 'vite';
 | 
					import { type UserConfig, defineConfig } from 'vite';
 | 
				
			||||||
import { configDefaults as vitestConfigDefaults } from 'vitest/config';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import locales from '../../locales';
 | 
					import locales from '../../locales';
 | 
				
			||||||
import meta from '../../package.json';
 | 
					import meta from '../../package.json';
 | 
				
			||||||
| 
						 | 
					@ -38,7 +37,7 @@ function toBase62(n: number): string {
 | 
				
			||||||
	return result;
 | 
						return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig(({ command, mode }) => {
 | 
					export function getConfig(): UserConfig {
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		base: '/vite/',
 | 
							base: '/vite/',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,7 +61,7 @@ export default defineConfig(({ command, mode }) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		css: {
 | 
							css: {
 | 
				
			||||||
			modules: {
 | 
								modules: {
 | 
				
			||||||
				generateScopedName: (name, filename, css) => {
 | 
									generateScopedName(name, filename, _css): string {
 | 
				
			||||||
					const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, '');
 | 
										const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, '');
 | 
				
			||||||
					if (process.env.NODE_ENV === 'production') {
 | 
										if (process.env.NODE_ENV === 'production') {
 | 
				
			||||||
						return 'x' + toBase62(hash(id)).substring(0, 4);
 | 
											return 'x' + toBase62(hash(id)).substring(0, 4);
 | 
				
			||||||
| 
						 | 
					@ -132,4 +131,8 @@ export default defineConfig(({ command, mode }) => {
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
});
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const config = defineConfig(({ command, mode }) => getConfig());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default config;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,11 +21,11 @@
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"devDependencies": {
 | 
						"devDependencies": {
 | 
				
			||||||
		"@microsoft/api-extractor": "7.34.4",
 | 
							"@microsoft/api-extractor": "7.34.4",
 | 
				
			||||||
 | 
							"@swc/jest": "0.2.24",
 | 
				
			||||||
		"@types/jest": "29.5.0",
 | 
							"@types/jest": "29.5.0",
 | 
				
			||||||
		"@types/node": "18.15.11",
 | 
							"@types/node": "18.15.11",
 | 
				
			||||||
		"@typescript-eslint/eslint-plugin": "5.57.0",
 | 
							"@typescript-eslint/eslint-plugin": "5.57.0",
 | 
				
			||||||
		"@typescript-eslint/parser": "5.57.0",
 | 
							"@typescript-eslint/parser": "5.57.0",
 | 
				
			||||||
		"@swc/jest": "0.2.24",
 | 
					 | 
				
			||||||
		"eslint": "8.37.0",
 | 
							"eslint": "8.37.0",
 | 
				
			||||||
		"jest": "^29.5.0",
 | 
							"jest": "^29.5.0",
 | 
				
			||||||
		"jest-fetch-mock": "^3.0.3",
 | 
							"jest-fetch-mock": "^3.0.3",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5580
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										5580
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue