Initial import
This commit is contained in:
14
src/features/layout/README.md
Executable file
14
src/features/layout/README.md
Executable file
@@ -0,0 +1,14 @@
|
||||
# Layout Feature
|
||||
|
||||
프로젝트 종속적인 레이아웃은 `src/features/layout` 아래에서 관리합니다.
|
||||
|
||||
## 포함 항목
|
||||
|
||||
- 컴포넌트 샘플 레이아웃
|
||||
- 위젯 샘플 레이아웃
|
||||
- Markdown preview 리스트 레이아웃
|
||||
|
||||
## 규칙
|
||||
|
||||
- 공통 레이아웃 시스템이 아니라 현재 프로젝트 화면에 종속되면 `features/layout`에 배치
|
||||
- 공통 재사용 가능성이 높으면 `components` 또는 `widgets`로 승격 검토
|
||||
178
src/features/layout/component-sample-gallery/ComponentSamplesLayout.tsx
Executable file
178
src/features/layout/component-sample-gallery/ComponentSamplesLayout.tsx
Executable file
@@ -0,0 +1,178 @@
|
||||
import { Anchor, Card, Empty, Flex, Space, Tag, Typography } from 'antd';
|
||||
import { createElement, useEffect, useMemo, useState } from 'react';
|
||||
import type { LoadedSampleEntry, SampleEntry } from '../../../samples/registry';
|
||||
import { resolveSampleEntries } from '../../../samples/registry';
|
||||
|
||||
const { Paragraph, Text, Title } = Typography;
|
||||
|
||||
export type ComponentSamplesLayoutProps = {
|
||||
entries: SampleEntry[];
|
||||
pathFilter?: string;
|
||||
excludeComponentIds?: string[];
|
||||
includeComponentIds?: string[];
|
||||
};
|
||||
|
||||
export function ComponentSamplesLayout({
|
||||
entries,
|
||||
pathFilter = '/components/',
|
||||
excludeComponentIds = [],
|
||||
includeComponentIds = [],
|
||||
}: ComponentSamplesLayoutProps) {
|
||||
const [sampleEntries, setSampleEntries] = useState<LoadedSampleEntry[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
void resolveSampleEntries(entries, pathFilter).then((loadedEntries) => {
|
||||
if (mounted) {
|
||||
setSampleEntries(loadedEntries);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [entries, pathFilter]);
|
||||
const groupedComponents = useMemo(() => {
|
||||
const excludedComponentIdSet = new Set(excludeComponentIds);
|
||||
const includedComponentIdSet = includeComponentIds.length ? new Set(includeComponentIds) : null;
|
||||
const componentMap = new Map<
|
||||
string,
|
||||
{
|
||||
componentId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
baseSample?: (typeof sampleEntries)[number];
|
||||
pluginSamples: (typeof sampleEntries)[number][];
|
||||
featureSamples: (typeof sampleEntries)[number][];
|
||||
}
|
||||
>();
|
||||
|
||||
sampleEntries.forEach((entry) => {
|
||||
if (excludedComponentIdSet.has(entry.sampleMeta.componentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (includedComponentIdSet && !includedComponentIdSet.has(entry.sampleMeta.componentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingGroup = componentMap.get(entry.sampleMeta.componentId) ?? {
|
||||
componentId: entry.sampleMeta.componentId,
|
||||
title: entry.sampleMeta.title,
|
||||
description: entry.sampleMeta.description,
|
||||
category: entry.sampleMeta.category,
|
||||
pluginSamples: [],
|
||||
featureSamples: [],
|
||||
};
|
||||
|
||||
if (entry.sampleMeta.kind === 'base') {
|
||||
existingGroup.baseSample = entry;
|
||||
existingGroup.title = entry.sampleMeta.title;
|
||||
existingGroup.description = entry.sampleMeta.description;
|
||||
existingGroup.category = entry.sampleMeta.category;
|
||||
} else if (entry.sampleMeta.kind === 'feature') {
|
||||
existingGroup.featureSamples.push(entry);
|
||||
} else {
|
||||
existingGroup.pluginSamples.push(entry);
|
||||
}
|
||||
|
||||
componentMap.set(entry.sampleMeta.componentId, existingGroup);
|
||||
});
|
||||
|
||||
return Array.from(componentMap.values());
|
||||
}, [excludeComponentIds, includeComponentIds, sampleEntries]);
|
||||
|
||||
if (groupedComponents.length === 0) {
|
||||
return <Empty description="표시할 컴포넌트 샘플이 없습니다." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="component-samples-layout">
|
||||
<aside className="component-samples-layout__sidebar">
|
||||
<Card title="Components" className="component-samples-layout__nav-card">
|
||||
<Anchor
|
||||
affix={false}
|
||||
items={groupedComponents.map((componentGroup) => ({
|
||||
key: componentGroup.componentId,
|
||||
href: `#component-sample-${componentGroup.componentId}`,
|
||||
title: componentGroup.title,
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
</aside>
|
||||
|
||||
<div className="component-samples-layout__content">
|
||||
<Flex vertical gap={20} className="component-samples-layout__stack">
|
||||
{groupedComponents.map((componentGroup) => (
|
||||
<Card
|
||||
key={componentGroup.componentId}
|
||||
id={`component-sample-${componentGroup.componentId}`}
|
||||
data-focus-id={`component:${componentGroup.componentId}`}
|
||||
title={componentGroup.title}
|
||||
extra={<Tag color="blue">{componentGroup.category}</Tag>}
|
||||
className="component-samples-layout__card"
|
||||
>
|
||||
<Space direction="vertical" size={16} className="component-samples-layout__section">
|
||||
<div>
|
||||
<Paragraph>{componentGroup.description}</Paragraph>
|
||||
<Text type="secondary" code>
|
||||
{componentGroup.componentId}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{componentGroup.baseSample ? (
|
||||
<div className="component-samples-layout__sample-block">
|
||||
<Title level={5}>Base Sample</Title>
|
||||
<div>{createElement(componentGroup.baseSample.Sample)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{componentGroup.pluginSamples.length > 0 ? (
|
||||
<div className="component-samples-layout__sample-block">
|
||||
<Title level={5}>Plugin Samples</Title>
|
||||
<Flex vertical gap={16}>
|
||||
{componentGroup.pluginSamples.map(({ modulePath, Sample, sampleMeta }) => (
|
||||
<Card
|
||||
key={modulePath}
|
||||
size="small"
|
||||
data-focus-id={`component:${componentGroup.componentId}:${sampleMeta.id}`}
|
||||
title={sampleMeta.title}
|
||||
extra={<Text code>{sampleMeta.variantLabel ?? sampleMeta.id}</Text>}
|
||||
>
|
||||
<Paragraph>{sampleMeta.description}</Paragraph>
|
||||
{createElement(Sample)}
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{componentGroup.featureSamples.length > 0 ? (
|
||||
<div className="component-samples-layout__sample-block">
|
||||
<Title level={5}>Feature Samples</Title>
|
||||
<Flex vertical gap={16}>
|
||||
{componentGroup.featureSamples.map(({ modulePath, Sample, sampleMeta }) => (
|
||||
<Card
|
||||
key={modulePath}
|
||||
size="small"
|
||||
data-focus-id={`component:${componentGroup.componentId}:${sampleMeta.id}`}
|
||||
title={sampleMeta.title}
|
||||
extra={<Text code>{sampleMeta.variantLabel ?? sampleMeta.id}</Text>}
|
||||
>
|
||||
<Paragraph>{sampleMeta.description}</Paragraph>
|
||||
{createElement(Sample)}
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
) : null}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
src/features/layout/component-sample-gallery/index.ts
Executable file
2
src/features/layout/component-sample-gallery/index.ts
Executable file
@@ -0,0 +1,2 @@
|
||||
export { ComponentSamplesLayout } from './ComponentSamplesLayout';
|
||||
export type { ComponentSamplesLayoutProps } from './ComponentSamplesLayout';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Flex } from 'antd';
|
||||
import { TmsDashboardFeatureSamples } from '../../dashboard/TmsDashboardFeatureSamples';
|
||||
import { WmsDashboardFeatureSamples } from '../../dashboard/WmsDashboardFeatureSamples';
|
||||
|
||||
export function DashboardFeatureGalleryLayout() {
|
||||
return (
|
||||
<Flex vertical gap={20} className="feature-dashboard-gallery">
|
||||
<WmsDashboardFeatureSamples />
|
||||
<TmsDashboardFeatureSamples />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
1
src/features/layout/dashboard-feature-gallery/index.ts
Executable file
1
src/features/layout/dashboard-feature-gallery/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export { DashboardFeatureGalleryLayout } from './DashboardFeatureGalleryLayout';
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Empty, Flex } from 'antd';
|
||||
import { createElement, useEffect, useState } from 'react';
|
||||
import { widgetSampleEntries } from '../../../app/manifests/samples.manifest';
|
||||
import type { LoadedSampleEntry } from '../../../samples/registry';
|
||||
import { resolveSampleEntries } from '../../../samples/registry';
|
||||
|
||||
export function DashboardReportGalleryLayout() {
|
||||
const [dashboardSamples, setDashboardSamples] = useState<LoadedSampleEntry[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
void resolveSampleEntries(widgetSampleEntries, '/widgets/').then((loadedEntries) => {
|
||||
if (mounted) {
|
||||
setDashboardSamples(
|
||||
loadedEntries.filter((entry) => entry.sampleMeta.componentId === 'dashboard-report-card'),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (dashboardSamples.length === 0) {
|
||||
return <Empty description="표시할 대시보드 카드 샘플이 없습니다." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={20} wrap className="dashboard-widget-grid">
|
||||
{dashboardSamples.map(({ modulePath, Sample }) => (
|
||||
<div key={modulePath} className="dashboard-widget-grid__item">
|
||||
{createElement(Sample)}
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
1
src/features/layout/dashboard-report-gallery/index.ts
Executable file
1
src/features/layout/dashboard-report-gallery/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export { DashboardReportGalleryLayout } from './DashboardReportGalleryLayout';
|
||||
87
src/features/layout/docs-markdown-preview/DocsMarkdownPreviewLayout.tsx
Executable file
87
src/features/layout/docs-markdown-preview/DocsMarkdownPreviewLayout.tsx
Executable file
@@ -0,0 +1,87 @@
|
||||
import { Card, Empty, Flex, Typography } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { docsMarkdownEntries } from '../../../app/manifests/docs.manifest';
|
||||
import { FolderTreeNav } from '../../../components/navigation';
|
||||
import {
|
||||
MarkdownPreviewCard,
|
||||
resolveMarkdownDocuments,
|
||||
type MarkdownDocument,
|
||||
} from '../../../components/markdownPreview';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export function DocsMarkdownPreviewLayout() {
|
||||
const [documents, setDocuments] = useState<MarkdownDocument[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
void resolveMarkdownDocuments(docsMarkdownEntries, '/docs/').then((loadedDocuments) => {
|
||||
if (mounted) {
|
||||
setDocuments(loadedDocuments);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
const grouped = useMemo(() => {
|
||||
const folderMap = new Map<string, typeof documents>();
|
||||
|
||||
documents.forEach((document) => {
|
||||
const bucket = folderMap.get(document.folder) ?? [];
|
||||
bucket.push(document);
|
||||
folderMap.set(document.folder, bucket);
|
||||
});
|
||||
|
||||
return Array.from(folderMap.entries()).sort((left, right) => left[0].localeCompare(right[0]));
|
||||
}, [documents]);
|
||||
|
||||
if (documents.length === 0) {
|
||||
return <Empty description="표시할 docs markdown 문서가 없습니다." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="docs-markdown-layout">
|
||||
<aside className="docs-markdown-layout__sidebar">
|
||||
<Card title="Docs Folders" className="component-samples-layout__nav-card">
|
||||
<FolderTreeNav
|
||||
title="Docs Tree"
|
||||
groups={grouped.map(([folder, folderDocuments]) => ({
|
||||
id: `docs-folder-${folder}`,
|
||||
label: folder,
|
||||
items: folderDocuments.map((document) => ({
|
||||
id: document.id,
|
||||
label: document.title,
|
||||
href: `#doc-item-${document.id}`,
|
||||
})),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
</aside>
|
||||
|
||||
<div className="docs-markdown-layout__content">
|
||||
<Flex vertical gap={20}>
|
||||
{grouped.map(([folder, folderDocuments]) => (
|
||||
<section key={folder} id={`docs-folder-${folder}`}>
|
||||
<Card title={folder} extra={<Text code>{folderDocuments.length} docs</Text>}>
|
||||
<Flex vertical gap={16}>
|
||||
{folderDocuments.map((document) => (
|
||||
<div
|
||||
key={document.id}
|
||||
id={`doc-item-${document.id}`}
|
||||
className="feature-markdown-list__item"
|
||||
>
|
||||
<MarkdownPreviewCard document={document} />
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
</Card>
|
||||
</section>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
src/features/layout/docs-markdown-preview/index.ts
Executable file
1
src/features/layout/docs-markdown-preview/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export { DocsMarkdownPreviewLayout } from './DocsMarkdownPreviewLayout';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { featureMarkdownEntries } from '../../../app/manifests/docs.manifest';
|
||||
import { MarkdownPreviewList } from '../../../components/markdownPreview';
|
||||
|
||||
export function FeatureMarkdownPreviewListLayout() {
|
||||
return (
|
||||
<MarkdownPreviewList
|
||||
entries={featureMarkdownEntries}
|
||||
basePath="/features/"
|
||||
emptyDescription="표시할 feature markdown 문서가 없습니다."
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/features/layout/feature-markdown-preview/index.ts
Executable file
1
src/features/layout/feature-markdown-preview/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export { FeatureMarkdownPreviewListLayout } from './FeatureMarkdownPreviewListLayout';
|
||||
33
src/features/layout/widget-registry-gallery/WidgetRegistryLayout.tsx
Executable file
33
src/features/layout/widget-registry-gallery/WidgetRegistryLayout.tsx
Executable file
@@ -0,0 +1,33 @@
|
||||
import { Card, Empty, Flex, List, Tag, Typography } from 'antd';
|
||||
import { registeredWidgets } from '../../../widgets/registry';
|
||||
import { resolveWidgetFeatures } from '../../../widgets/core';
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
export function WidgetRegistryLayout() {
|
||||
if (registeredWidgets.length === 0) {
|
||||
return <Empty description="등록된 위젯이 없습니다." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={20} wrap className="sample-widgets-layout">
|
||||
{registeredWidgets.map((widget) => (
|
||||
<div key={widget.id} className="sample-widgets-layout__item">
|
||||
<Card title={widget.title} extra={<Text code>{widget.id}</Text>}>
|
||||
<Paragraph>{widget.description}</Paragraph>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={resolveWidgetFeatures(widget.features)}
|
||||
renderItem={(feature) => (
|
||||
<List.Item>
|
||||
<Tag color="blue">{feature.label}</Tag>
|
||||
<span>{feature.description}</span>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
1
src/features/layout/widget-registry-gallery/index.ts
Executable file
1
src/features/layout/widget-registry-gallery/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export { WidgetRegistryLayout } from './WidgetRegistryLayout';
|
||||
56
src/features/layout/widget-sample-gallery/SampleWidgetsLayout.tsx
Executable file
56
src/features/layout/widget-sample-gallery/SampleWidgetsLayout.tsx
Executable file
@@ -0,0 +1,56 @@
|
||||
import { Empty, Flex } from 'antd';
|
||||
import { createElement, useEffect, useState } from 'react';
|
||||
import type { LoadedSampleEntry, SampleEntry } from '../../../samples/registry';
|
||||
import { resolveSampleEntries } from '../../../samples/registry';
|
||||
|
||||
export type SampleWidgetsLayoutProps = {
|
||||
entries: SampleEntry[];
|
||||
pathFilter?: string;
|
||||
includeComponentIds?: string[];
|
||||
};
|
||||
|
||||
export function SampleWidgetsLayout({
|
||||
entries,
|
||||
pathFilter = '/widgets/',
|
||||
includeComponentIds = [],
|
||||
}: SampleWidgetsLayoutProps) {
|
||||
const [sampleEntries, setSampleEntries] = useState<LoadedSampleEntry[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
void resolveSampleEntries(entries, pathFilter).then((loadedEntries) => {
|
||||
if (mounted) {
|
||||
setSampleEntries(loadedEntries);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [entries, pathFilter]);
|
||||
|
||||
const visibleEntries =
|
||||
includeComponentIds.length > 0
|
||||
? sampleEntries.filter((entry) => includeComponentIds.includes(entry.sampleMeta.componentId))
|
||||
: sampleEntries;
|
||||
|
||||
if (visibleEntries.length === 0) {
|
||||
return <Empty description="표시할 위젯 샘플이 없습니다." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={20} wrap className="sample-widgets-layout">
|
||||
{visibleEntries.map(({ modulePath, Sample, sampleMeta }) => (
|
||||
<div
|
||||
key={modulePath}
|
||||
id={`widget-sample-${sampleMeta.componentId}`}
|
||||
className="sample-widgets-layout__item"
|
||||
data-focus-id={`widget:${sampleMeta.componentId}`}
|
||||
>
|
||||
<div className="sample-widgets-layout__item">{createElement(Sample)}</div>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
2
src/features/layout/widget-sample-gallery/index.ts
Executable file
2
src/features/layout/widget-sample-gallery/index.ts
Executable file
@@ -0,0 +1,2 @@
|
||||
export { SampleWidgetsLayout } from './SampleWidgetsLayout';
|
||||
export type { SampleWidgetsLayoutProps } from './SampleWidgetsLayout';
|
||||
Reference in New Issue
Block a user