* 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:
committed by
GitHub
parent
8a0201fe9c
commit
38d0b62167
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"]
|
||||
}
|
||||
Reference in New Issue
Block a user