Initial commit

Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { Launcher } from './Launcher';
export default {
title: 'Preview/Launcher/[WIP] Launcher',
component: Launcher,
argTypes: {}
} as ComponentMeta<typeof Launcher>;
const Template: ComponentStory<typeof Launcher> = (args) => <Launcher {...args}></Launcher>;
export const Primary = Template.bind({});
Primary.args = {};

View File

@@ -0,0 +1,138 @@
import React, { useState } from 'react';
import { IconName } from '@noodl-core-ui/components/common/Icon';
import {
CloudSyncType,
LauncherProjectData
} from '@noodl-core-ui/preview/launcher/Launcher/components/LauncherProjectCard';
import { LauncherSidebar } from '@noodl-core-ui/preview/launcher/Launcher/components/LauncherSidebar';
import { LearningCenter } from '@noodl-core-ui/preview/launcher/Launcher/views/LearningCenter';
import { Projects } from '@noodl-core-ui/preview/launcher/Launcher/views/Projects';
import { LauncherApp } from '../../template/LauncherApp';
export interface LauncherProps {}
export enum LauncherPageId {
LocalProjects,
LearningCenter
}
export interface LauncherPageMetaData {
id: LauncherPageId | string; // renders workspace page if starts with WORKSPACE_PAGE_PREFIX
displayName: string;
icon?: IconName;
}
// FIXME: make the mock data real
export const PAGES: LauncherPageMetaData[] = [
{
id: LauncherPageId.LocalProjects,
displayName: 'Recent Projects',
icon: IconName.CircleDot
},
{
id: LauncherPageId.LearningCenter,
displayName: 'Learn',
icon: IconName.Rocket
}
];
export const MOCK_PROJECTS: LauncherProjectData[] = [
{
id: '1',
title: 'My first project',
imageSrc: 'http://placekitten.com/g/200/300',
localPath: '/User/Desktop/dev/my-first-project',
lastOpened: '2023-10-26T12:22:13.462Z',
cloudSyncMeta: {
type: CloudSyncType.None,
source: undefined
},
uncommittedChangesAmount: undefined,
pullAmount: undefined,
pushAmount: undefined
},
{
id: '2',
title: 'External git project with push but no pull',
imageSrc: 'http://placekitten.com/g/400/800',
localPath: '/User/Desktop/dev/area-51-employee-portal/top-secret-version',
lastOpened: '2023-10-23T12:42:13.462Z',
cloudSyncMeta: {
type: CloudSyncType.Git,
source: 'https://TESTHUB.com/org/testcompany/my-repo-project'
},
uncommittedChangesAmount: undefined,
pullAmount: undefined,
pushAmount: 666,
contributors: [
{ email: 'tore@noodl.net', name: 'Tore Knudsen', id: 'Tore' },
{ email: 'eric@noodl.net', name: 'Eric Tuvesson', id: 'Eric' }
]
},
{
id: '3',
title: 'External git project with local changes',
imageSrc: 'http://placekitten.com/g/500/500',
localPath: '/User/Desktop/projects/my-git-repo',
lastOpened: '2023-08-26T12:42:13.462Z',
cloudSyncMeta: {
type: CloudSyncType.Git,
source: 'https://TESTHUB.com/org/testcompany/my-repo-project'
},
uncommittedChangesAmount: 4,
pullAmount: undefined,
pushAmount: undefined,
contributors: [
{ email: 'tore@noodl.net', name: 'Tore Knudsen', id: 'Tore' },
{ email: 'eric@noodl.net', name: 'Eric Tuvesson', id: 'Eric' },
{ email: 'michael@noodl.net', name: 'Michael Cartner', id: 'Michael' },
{ email: 'mikael@noodl.net', name: 'Mikael Tellhed', id: 'Mikael' },
{ email: 'anders@noodl.net', name: 'Anders Larsson', id: 'Anders' },
{ email: 'johan@noodl.net', name: 'Johan Olsson', id: 'Johan' },
{ email: 'victor@noodl.net', name: 'Victor Permild', id: 'Victor' },
{ email: 'kotte@noodl.net', name: 'Kotte Aistre', id: 'Kotte' }
]
},
{
id: '4',
title: 'Git project with all notifications',
imageSrc: 'http://placekitten.com/g/100/100',
localPath: '/User/Desktop/projects/forgotten-project',
lastOpened: '2023-06-26T12:42:13.462Z',
cloudSyncMeta: {
type: CloudSyncType.Git
},
uncommittedChangesAmount: 10,
pullAmount: 10,
pushAmount: 4,
contributors: [
{ email: 'tore@noodl.net', name: 'Tore Knudsen', id: 'Tore' },
{ email: 'eric@noodl.net', name: 'Eric Tuvesson', id: 'Eric' },
{ email: 'michael@noodl.net', name: 'Michael Cartner', id: 'Michael' },
{ email: 'victor@noodl.net', name: 'Victor Permild', id: 'Victor' }
]
}
];
export function Launcher({}: LauncherProps) {
const pages = [...PAGES];
const [activePageId, setActivePageId] = useState<LauncherPageMetaData['id']>(pages[0].id);
function setActivePage(pageId: LauncherPageMetaData['id']) {
setActivePageId(pageId);
console.info(`Navigated to pageId ${pageId}`);
}
const activePage = pages.find((page) => page.id === activePageId);
return (
<LauncherApp
sidePanel={<LauncherSidebar pages={pages} activePageId={activePageId} setActivePageId={setActivePage} />}
>
{activePageId === LauncherPageId.LocalProjects && <Projects />}
{activePageId === LauncherPageId.LearningCenter && <LearningCenter />}
</LauncherApp>
);
}

View File

@@ -0,0 +1,5 @@
.Root {
margin: 0 auto;
max-width: 800px;
margin-top: 82px;
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Box } from '@noodl-core-ui/components/layout/Box';
import { HStack } from '@noodl-core-ui/components/layout/Stack';
import { Title, TitleSize, TitleVariant } from '@noodl-core-ui/components/typography/Title';
import css from './LauncherPage.module.scss';
export interface LauncherPageProps {
title: string;
children?: JSX.Element | JSX.Element[];
headerSlot?: JSX.Element | JSX.Element[];
}
export function LauncherPage({ title, children, headerSlot }: LauncherPageProps) {
return (
<div className={css['Root']}>
<Box hasBottomSpacing={14}>
<HStack UNSAFE_style={{ justifyContent: 'space-between' }}>
<Title size={TitleSize.Large} variant={TitleVariant.Highlighted}>
{title}
</Title>
<div>{headerSlot}</div>
</HStack>
</Box>
{children}
</div>
);
}

View File

@@ -0,0 +1 @@
export { LauncherPage } from './LauncherPage';

View File

@@ -0,0 +1,19 @@
.Image {
width: 100px;
margin: -16px;
margin-right: 16px;
border-radius: 2px 0 0 2px;
background-size: cover;
}
.Details {
width: 100%;
}
.TypeDisplay {
margin-bottom: 8px;
}
.VersionControlTooltip {
cursor: default;
}

View File

@@ -0,0 +1,209 @@
import React from 'react';
import { FeedbackType } from '@noodl-constants/FeedbackType';
import { Card, CardBackground } from '@noodl-core-ui/components/common/Card';
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
import { IconButton } from '@noodl-core-ui/components/inputs/IconButton';
import { PrimaryButton, PrimaryButtonSize, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
import { TextButton, TextButtonSize } from '@noodl-core-ui/components/inputs/TextButton';
import { DialogRenderDirection } from '@noodl-core-ui/components/layout/BaseDialog';
import { Box } from '@noodl-core-ui/components/layout/Box';
import { Columns } from '@noodl-core-ui/components/layout/Columns';
import { HStack, Stack, VStack } from '@noodl-core-ui/components/layout/Stack';
import { ContextMenu, ContextMenuProps } from '@noodl-core-ui/components/popups/ContextMenu';
import { Tooltip } from '@noodl-core-ui/components/popups/Tooltip';
import { Label, LabelSize, LabelSpacingSize } from '@noodl-core-ui/components/typography/Label';
import { Text, TextSize, TextType } from '@noodl-core-ui/components/typography/Text';
import { Title, TitleSize } from '@noodl-core-ui/components/typography/Title';
import { UserBadgeProps, UserBadgeSize } from '@noodl-core-ui/components/user/UserBadge';
import { UserBadgeList } from '@noodl-core-ui/components/user/UserBadgeList';
import css from './LauncherProjectCard.module.scss';
// FIXME: Use the timeSince function from the editor package when this is moved there
function timeSince(date: Date | number) {
const date_unix = typeof date === 'number' ? date : date.getTime();
var seconds = Math.floor((new Date().getTime() - date_unix) / 1000);
var interval = Math.floor(seconds / 31536000);
if (interval > 1) {
return interval + ' years';
}
interval = Math.floor(seconds / 2592000);
if (interval > 1) {
return interval + ' months';
}
interval = Math.floor(seconds / 86400);
if (interval > 1) {
return interval + ' days';
}
interval = Math.floor(seconds / 3600);
if (interval > 1) {
return interval + ' hours';
}
interval = Math.floor(seconds / 60);
if (interval > 1) {
return interval + ' minutes';
}
return Math.floor(seconds) + ' seconds';
}
export enum CloudSyncType {
None = 'Local',
Git = 'Git'
}
export interface LauncherProjectData {
id: string;
title: string;
cloudSyncMeta: {
type: CloudSyncType;
source?: string;
};
localPath: string;
lastOpened: string;
pullAmount?: number;
pushAmount?: number;
uncommittedChangesAmount?: number;
imageSrc: string;
contributors?: UserBadgeProps[];
}
export interface LauncherProjectCardProps extends LauncherProjectData {
contextMenuItems: ContextMenuProps[];
}
export function LauncherProjectCard({
id,
title,
cloudSyncMeta,
lastOpened,
pullAmount,
pushAmount,
uncommittedChangesAmount,
imageSrc,
contextMenuItems,
contributors
}: LauncherProjectCardProps) {
return (
<Card
background={CardBackground.Bg2}
hoverBackground={CardBackground.Bg3}
onClick={() => alert('FIXME: open project')}
>
<Stack direction="row">
<div className={css.Image} style={{ backgroundImage: `url(${imageSrc})` }} />
<div className={css.Details}>
<Columns layoutString="1 1 1" hasXGap={4}>
<div>
<Title hasBottomSpacing size={TitleSize.Medium}>
{title}
</Title>
<Label variant={TextType.Shy}>Last opened {timeSince(new Date(lastOpened))} ago</Label>
</div>
<div>
{cloudSyncMeta.type === CloudSyncType.None && (
<div>
<Label hasBottomSpacing>None</Label>
<HStack UNSAFE_style={{ alignItems: 'center' }} hasSpacing={1}>
<Icon icon={IconName.WarningCircle} variant={TextType.Shy} size={IconSize.Tiny} />
<Label variant={TextType.Shy}>Project is only local</Label>
</HStack>
</div>
)}
{cloudSyncMeta.type === CloudSyncType.Git && (
<div className={css.TypeDisplay}>
<TextButton
label="Open Git repo"
size={TextButtonSize.Small}
icon={IconName.ExternalLink}
onClick={(e) => {
e.stopPropagation();
alert('FIXME: Link to repo?');
}}
/>
</div>
)}
<HStack hasSpacing={4} UNSAFE_style={{ paddingLeft: 4 }}>
{Boolean(pullAmount) && (
<Tooltip
content={`${pullAmount} unpulled commits`}
showAfterMs={200}
UNSAFE_className={css.VersionControlTooltip}
>
<HStack UNSAFE_style={{ alignItems: 'center' }}>
<Icon icon={IconName.CloudDownload} variant={FeedbackType.Notice} size={IconSize.Tiny} />
<Label hasLeftSpacing={LabelSpacingSize.Small} variant={FeedbackType.Notice}>
{pullAmount}
</Label>
</HStack>
</Tooltip>
)}
{Boolean(pushAmount) && (
<Tooltip
content={`${pushAmount} unpushed local commits`}
showAfterMs={200}
UNSAFE_className={css.VersionControlTooltip}
>
<HStack UNSAFE_style={{ alignItems: 'center' }}>
<Icon icon={IconName.CloudUpload} variant={FeedbackType.Danger} size={IconSize.Tiny} />
<Label hasLeftSpacing={LabelSpacingSize.Small} variant={FeedbackType.Danger}>
{pushAmount}
</Label>
</HStack>
</Tooltip>
)}
{Boolean(uncommittedChangesAmount) && (
<Tooltip
content={`${uncommittedChangesAmount} uncommitted changes`}
showAfterMs={200}
UNSAFE_className={css.VersionControlTooltip}
>
<HStack UNSAFE_style={{ alignItems: 'center' }}>
<Icon
icon={IconName.WarningCircle}
variant={FeedbackType.Danger}
size={IconSize.Tiny}
UNSAFE_className={css.VersionControlTooltip}
/>
<Label hasLeftSpacing={LabelSpacingSize.Small} variant={FeedbackType.Danger}>
{uncommittedChangesAmount}
</Label>
</HStack>
</Tooltip>
)}
</HStack>
</div>
<HStack UNSAFE_style={{ justifyContent: 'space-between', alignItems: 'center' }} hasSpacing={4}>
<HStack UNSAFE_style={{ alignItems: 'center' }} hasSpacing={2}>
{/* FIXME: get default user data from user object */}
<UserBadgeList
badges={contributors || [{ name: 'Tore Knudsen', email: 'tore@noodl.net', id: 'Tore' }]}
size={UserBadgeSize.Medium}
maxVisible={4}
/>
{!Boolean(contributors) && <Label variant={TextType.Shy}>(Only you)</Label>}
</HStack>
{Boolean(contextMenuItems) && (
<div>
<ContextMenu renderDirection={DialogRenderDirection.Below} menuItems={contextMenuItems} />
</div>
)}
</HStack>
</Columns>
</div>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1 @@
export * from './LauncherProjectCard'

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { LauncherSearchBar } from './LauncherSearchBar';
export default {
title: 'CATEGORY_HERE/LauncherSearchBar',
component: LauncherSearchBar,
argTypes: {},
} as ComponentMeta<typeof LauncherSearchBar>;
const Template: ComponentStory<typeof LauncherSearchBar> = (args) => <LauncherSearchBar {...args} />;
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { Icon, IconName } from '@noodl-core-ui/components/common/Icon';
import { Select, SelectColorTheme, SelectOption } from '@noodl-core-ui/components/inputs/Select';
import { TextInput, TextInputVariant } from '@noodl-core-ui/components/inputs/TextInput';
import { Box } from '@noodl-core-ui/components/layout/Box';
import { HStack } from '@noodl-core-ui/components/layout/Stack';
import { TextType } from '@noodl-core-ui/components/typography/Text';
interface UseLauncherSearchBarProps {
filterDropdownItems: SelectOption[];
propertyNameToFilter: TSFixme;
allItems: TSFixme;
}
interface LauncherSearchBarProps {
searchTerm: string;
setSearchTerm: (value: string) => void;
filterDropdownItems: UseLauncherSearchBarProps['filterDropdownItems'];
filterValue: SelectOption['value'];
setFilterValue: (value: SelectOption['value']) => void;
}
export function useLauncherSearchBar({
filterDropdownItems,
allItems,
propertyNameToFilter
}: UseLauncherSearchBarProps) {
const [searchTerm, setSearchTerm] = useState('');
const [filterValue, setFilterValue] = useState(filterDropdownItems[0].value);
function createFilterFunction(propertyName, value) {
return (item) => {
let propertyChain = propertyName.split('.');
let currentValue = item;
for (let prop of propertyChain) {
if (currentValue.hasOwnProperty(prop)) {
currentValue = currentValue[prop];
} else {
return false; // Property not found, filter it out
}
}
return currentValue === value;
};
}
const filteredItems =
filterValue !== 'all' ? allItems.filter(createFilterFunction(propertyNameToFilter, filterValue)) : allItems;
const searchedItems = Boolean(searchTerm)
? filteredItems.filter((project) => project.title.toLowerCase().includes(searchTerm.toLowerCase()))
: filteredItems;
return {
items: searchedItems,
filterValue,
setFilterValue,
searchTerm,
setSearchTerm
};
}
export function LauncherSearchBar({
searchTerm,
setSearchTerm,
filterDropdownItems,
setFilterValue,
filterValue
}: LauncherSearchBarProps) {
return (
<Box hasBottomSpacing>
<HStack hasSpacing UNSAFE_style={{ paddingBottom: 4, borderBottom: '1px solid var(--theme-color-bg-3)' }}>
<TextInput
slotBeforeInput={<Icon icon={IconName.Search} variant={TextType.Shy} />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.currentTarget.value)}
variant={TextInputVariant.Transparent}
/>
<div style={{ width: 220 }}>
<Select
options={filterDropdownItems}
onChange={setFilterValue}
value={filterValue}
colorTheme={SelectColorTheme.Transparent}
/>
</div>
</HStack>
</Box>
);
}

View File

@@ -0,0 +1 @@
export * from './LauncherSearchBar'

View File

@@ -0,0 +1,10 @@
.Root {
font-family: var(--font-family);
color: var(--theme-color-fg-default);
}
.WorkspaceIcon {
width: 14px;
height: 14px;
border-radius: 50%;
}

View File

@@ -0,0 +1,86 @@
import React from 'react';
import { Logo } from '@noodl-core-ui/components/common/Logo';
import { DialogRenderDirection } from '@noodl-core-ui/components/layout/BaseDialog';
import { Container, ContainerDirection } from '@noodl-core-ui/components/layout/Container';
import { ListItem } from '@noodl-core-ui/components/layout/ListItem';
import { HStack } from '@noodl-core-ui/components/layout/Stack';
import { ContextMenu } from '@noodl-core-ui/components/popups/ContextMenu';
import { Section } from '@noodl-core-ui/components/sidebar/Section';
import { Label, LabelSize, LabelSpacingSize } from '@noodl-core-ui/components/typography/Label';
import { TextType } from '@noodl-core-ui/components/typography/Text';
import { Title, TitleSize, TitleVariant } from '@noodl-core-ui/components/typography/Title';
import { UserBadge, UserBadgeSize } from '@noodl-core-ui/components/user/UserBadge';
import { LauncherPageMetaData } from '@noodl-core-ui/preview/launcher/Launcher/Launcher';
import { LauncherSection } from '@noodl-core-ui/preview/template/LauncherApp';
import css from './LauncherSidebar.module.scss';
const VERSION_NUMBER = '2.9.3';
export interface LauncherSidebarProps {
pages: LauncherPageMetaData[];
setActivePageId: (page: LauncherPageMetaData['id'] | string) => void;
activePageId: LauncherPageMetaData['id'];
}
export function LauncherSidebar({ pages, activePageId, setActivePageId }: LauncherSidebarProps) {
return (
<Container direction={ContainerDirection.Vertical} hasSpaceBetween>
<Container direction={ContainerDirection.Vertical}>
<LauncherSection>
<Logo />
</LauncherSection>
<LauncherSection>
<HStack UNSAFE_style={{ alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<Title variant={TitleVariant.Highlighted} size={TitleSize.Large}>
Noodl {VERSION_NUMBER}
</Title>
</div>
<ContextMenu
menuItems={[{ label: 'Check for updates', onClick: () => alert('FIXME: check updates') }]}
renderDirection={DialogRenderDirection.Horizontal}
/>
</HStack>
</LauncherSection>
<Section>
<div style={{ padding: '0 10px' }}>
<Container direction={ContainerDirection.Vertical} hasYSpacing>
{pages.map((page) => (
<ListItem
text={page.displayName}
icon={page.icon}
gutter={2}
onClick={() => setActivePageId(page.id)}
isActive={page.id === activePageId}
UNSAFE_style={{ borderRadius: 2 }}
/>
))}
</Container>
</div>
</Section>
<Section>
<div style={{ padding: '0 10px' }}>
<Container direction={ContainerDirection.Vertical} hasYSpacing>
<Label
size={LabelSize.Small}
variant={TextType.Shy}
hasBottomSpacing={LabelSpacingSize.Large}
UNSAFE_style={{ paddingLeft: 20 }}
>
Resources
</Label>
<ListItem gutter={5} text="Documentation" hasHiddenIconSlot UNSAFE_style={{ borderRadius: 2 }} />
<ListItem gutter={5} text="YouTube" hasHiddenIconSlot UNSAFE_style={{ borderRadius: 2 }} />
<ListItem gutter={5} text="Discord" hasHiddenIconSlot UNSAFE_style={{ borderRadius: 2 }} />
</Container>
</div>
</Section>
</Container>
</Container>
);
}

View File

@@ -0,0 +1 @@
export { LauncherSidebar } from './LauncherSidebar';

View File

@@ -0,0 +1,172 @@
import React, { useState } from 'react';
import { FeedbackType } from '@noodl-constants/FeedbackType';
import { Card, CardBackground } from '@noodl-core-ui/components/common/Card';
import { IconName } from '@noodl-core-ui/components/common/Icon';
import { PrimaryButton, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
import { TextButton } from '@noodl-core-ui/components/inputs/TextButton';
import { TextInput, TextInputVariant } from '@noodl-core-ui/components/inputs/TextInput';
import { Box } from '@noodl-core-ui/components/layout/Box';
import { Columns } from '@noodl-core-ui/components/layout/Columns';
import { Modal, ModalProps } from '@noodl-core-ui/components/layout/Modal';
import { HStack } from '@noodl-core-ui/components/layout/Stack';
import { useConfirmationDialog } from '@noodl-core-ui/components/popups/ConfirmationDialog/ConfirmationDialog.hooks';
import { Text } from '@noodl-core-ui/components/typography/Text';
import { Title } from '@noodl-core-ui/components/typography/Title';
import {
CloudSyncType,
LauncherProjectData
} from '@noodl-core-ui/preview/launcher/Launcher/components/LauncherProjectCard';
interface ProjectSettingsModalProps {
projectData?: LauncherProjectData;
isVisible: ModalProps['isVisible'];
onClose: ModalProps['onClose'];
}
export function ProjectSettingsModal({ isVisible, projectData, onClose }: ProjectSettingsModalProps) {
const [newProjectName, setNewProjectName] = useState(projectData?.title);
const [gitUrl, setGitUrl] = useState('');
const [SaveDialog, confirmSaveProjectData] = useConfirmationDialog({
title: 'Please confirm',
message: 'Are you sure you want to update the project settings?'
});
const [RemoveDialog, confirmRemoveWorkspaceProject] = useConfirmationDialog({
title: 'Please confirm',
message: 'Are you sure you want to remove the project?',
isDangerousAction: true
});
if (!projectData) return null;
function onPersistUpdatedData() {
confirmSaveProjectData()
.then(() => {
alert('FIXME: take all new values and persist them');
})
.catch(() => console.log('Save cancelled'));
}
function onPushToExternalGitRepo() {
alert('FIXME: Push to an external Git repo');
}
function onUnsyncProjectFromGit() {
alert('FIXME: Reset .git folder in project');
}
function onRemoveProject() {
confirmRemoveWorkspaceProject()
.then(() => {
alert('FIXME: Remove project');
})
.catch(() => {
console.log('Project removal cancelled');
});
}
return (
<Modal
isVisible={isVisible}
onClose={onClose}
title="Project settings"
subtitle={projectData.title}
footerSlot={
<Box hasBottomSpacing UNSAFE_style={{ width: '100%' }}>
<HStack hasSpacing UNSAFE_style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<TextButton
label="Remove project"
icon={IconName.Trash}
variant={FeedbackType.Danger}
onClick={onRemoveProject}
/>
<HStack hasSpacing>
<PrimaryButton label="Cancel" onClick={onClose} variant={PrimaryButtonVariant.Muted} />
<PrimaryButton label="Save changes" onClick={onPersistUpdatedData} />
</HStack>
</HStack>
</Box>
}
>
<SaveDialog />
<RemoveDialog />
<Columns hasXGap>
<TextInput
value={newProjectName}
onChange={(e) => setNewProjectName(e.currentTarget.value)}
variant={TextInputVariant.InModal}
label="Project name"
/>
<TextInput
value={projectData.localPath}
variant={TextInputVariant.InModal}
label="Project folder path"
isReadonly
isCopyable
/>
</Columns>
{projectData.cloudSyncMeta.type === CloudSyncType.None && (
<>
<Box hasTopSpacing>
<Card background={CardBackground.Bg2}>
<Title hasBottomSpacing>This project is local only.</Title>
<Text>
Please create a Git repository in your preferred Git provider, and push the repository to enable
collaboration and version control for this project.
</Text>
<HStack hasSpacing>
<TextInput
value={gitUrl}
onChange={(e) => setGitUrl(e.currentTarget.value)}
label="Git repo URL"
UNSAFE_style={{ paddingTop: 16 }}
/>
<div style={{ alignSelf: 'flex-end' }}>
<PrimaryButton label="Push project to Git" onClick={() => onPushToExternalGitRepo()} />
</div>
</HStack>
</Card>
</Box>
</>
)}
{projectData.cloudSyncMeta.type === CloudSyncType.Git && (
<Box hasTopSpacing>
<Card background={CardBackground.Bg2}>
<Title hasBottomSpacing>Project is synced to a Git repo</Title>
<Text hasBottomSpacing>
Versioning and branching can be managed inside of the Noodl editor. User access has to be managed with
your Git provider.
</Text>
<HStack hasSpacing>
<TextInput
value={projectData.cloudSyncMeta.source}
label="Current Git repository URL"
isReadonly
isCopyable
/>
<div style={{ alignSelf: 'flex-end' }}>
<PrimaryButton
label="Unsync project instance from Git"
onClick={() => onUnsyncProjectFromGit()}
variant={PrimaryButtonVariant.Danger}
/>
</div>
</HStack>
</Card>
</Box>
)}
</Modal>
);
}

View File

@@ -0,0 +1 @@
export * from './ProjectSettingsModal';

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { Title } from '@noodl-core-ui/components/typography/Title';
import { LauncherPage } from '@noodl-core-ui/preview/launcher/Launcher/components/LauncherPage';
export interface LearningCenterViewProps {}
export function LearningCenter({}: LearningCenterViewProps) {
return <LauncherPage title="Learning Center"></LauncherPage>;
}

View File

@@ -0,0 +1,145 @@
import React, { useRef, useState } from 'react';
import { Icon, IconName } from '@noodl-core-ui/components/common/Icon';
import { PrimaryButton, PrimaryButtonSize, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
import { Select, SelectColorTheme, SelectOption } from '@noodl-core-ui/components/inputs/Select';
import { TextInput, TextInputVariant } from '@noodl-core-ui/components/inputs/TextInput';
import { Box } from '@noodl-core-ui/components/layout/Box';
import { Columns } from '@noodl-core-ui/components/layout/Columns';
import { HStack } from '@noodl-core-ui/components/layout/Stack';
import { Label, LabelSize } from '@noodl-core-ui/components/typography/Label';
import { TextType } from '@noodl-core-ui/components/typography/Text';
import { LauncherPage } from '@noodl-core-ui/preview/launcher/Launcher/components/LauncherPage';
import {
CloudSyncType,
LauncherProjectCard,
LauncherProjectData
} from '@noodl-core-ui/preview/launcher/Launcher/components/LauncherProjectCard';
import {
LauncherSearchBar,
useLauncherSearchBar
} from '@noodl-core-ui/preview/launcher/Launcher/components/LauncherSearchBar';
import { ProjectSettingsModal } from '@noodl-core-ui/preview/launcher/Launcher/components/ProjectSettingsModal';
import { MOCK_PROJECTS } from '@noodl-core-ui/preview/launcher/Launcher/Launcher';
export interface ProjectsViewProps {}
export function Projects({}: ProjectsViewProps) {
const allProjects = MOCK_PROJECTS;
const [selectedProjectId, setSelectedProjectId] = useState(null);
const uniqueTypes = [...new Set(allProjects.map((item) => item.cloudSyncMeta.type))];
const visibleTypesDropdownItems: SelectOption[] = [
{ label: 'All projects', value: 'all' },
...uniqueTypes.map((type) => ({ label: `Only ${type.toLowerCase()} projects`, value: type }))
];
const {
items: projects,
filterValue,
setFilterValue,
searchTerm,
setSearchTerm
} = useLauncherSearchBar({
allItems: allProjects,
filterDropdownItems: visibleTypesDropdownItems,
propertyNameToFilter: 'cloudSyncMeta.type'
});
function onOpenProjectSettings(projectDataId: LauncherProjectData['id']) {
setSelectedProjectId(projectDataId);
}
function onCloseProjectSettings() {
setSelectedProjectId(null);
}
function onImportProjectClick() {
alert('FIXME: Import project');
}
function onNewProjectClick() {
alert('FIXME: Create new project');
}
return (
<LauncherPage
title="Recent Projects"
headerSlot={
<HStack hasSpacing>
<PrimaryButton
label="Open project"
size={PrimaryButtonSize.Small}
variant={PrimaryButtonVariant.Muted}
onClick={onImportProjectClick}
/>
<PrimaryButton label="Create new project" size={PrimaryButtonSize.Small} onClick={onNewProjectClick} />
</HStack>
}
>
<ProjectSettingsModal
isVisible={selectedProjectId !== null}
onClose={onCloseProjectSettings}
projectData={projects.find((project) => project.id === selectedProjectId)}
/>
<LauncherSearchBar
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
filterValue={filterValue}
setFilterValue={setFilterValue}
filterDropdownItems={visibleTypesDropdownItems}
/>
{/* TODO: make project list legend and grid reusable */}
<Box hasBottomSpacing={4} hasTopSpacing={4}>
<HStack hasSpacing>
<div style={{ width: 100 }} />
<div style={{ width: '100%' }}>
<Columns layoutString={'1 1 1'}>
<Label variant={TextType.Shy} size={LabelSize.Small}>
Name
</Label>
<Label variant={TextType.Shy} size={LabelSize.Small}>
Version control
</Label>
<Label variant={TextType.Shy} size={LabelSize.Small}>
Contributors
</Label>
</Columns>
</div>
</HStack>
</Box>
<Columns layoutString="1" hasXGap hasYGap>
{projects.map((project) => (
<LauncherProjectCard
key={project.id}
{...project}
contextMenuItems={[
{
label: 'Launch project',
onClick: () => alert('FIXME: Launch project')
},
{
label: 'Open project folder',
onClick: () => alert('FIXME: Open folder')
},
{
label: 'Open project settings',
onClick: () => onOpenProjectSettings(project.id)
},
'divider',
{
label: 'Delete project',
onClick: () => alert('FIXME: Delete project'),
icon: IconName.Trash,
isDangerous: true
}
]}
/>
))}
</Columns>
</LauncherPage>
);
}

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { Group } from './Group';
export default {
title: 'Preview/Property Panel/[WIP] Group',
component: Group,
argTypes: {}
} as ComponentMeta<typeof Group>;
const Template: ComponentStory<typeof Group> = (args) => <Group></Group>;
export const Primary = Template.bind({});
Primary.args = {};

View File

@@ -0,0 +1,471 @@
import React from 'react';
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
import { Box } from '@noodl-core-ui/components/layout/Box';
import { ScrollArea } from '@noodl-core-ui/components/layout/ScrollArea';
import { VStack } from '@noodl-core-ui/components/layout/Stack';
import { PropertyPanelButton } from '@noodl-core-ui/components/property-panel/PropertyPanelButton';
import { PropertyPanelCheckbox } from '@noodl-core-ui/components/property-panel/PropertyPanelCheckbox';
import {
PropertyPanelIconRadioInput,
PropertyPanelIconRadioSize
} from '@noodl-core-ui/components/property-panel/PropertyPanelIconRadioInput';
import {
PropertyPanelInput,
PropertyPanelInputType,
PropertyPanelRow
} from '@noodl-core-ui/components/property-panel/PropertyPanelInput';
import { PropertyPanelLengthUnitInput } from '@noodl-core-ui/components/property-panel/PropertyPanelLengthUnitInput';
import { PropertyPanelMarginPadding } from '@noodl-core-ui/components/property-panel/PropertyPanelMarginPadding';
import { PropertyPanelNumberInput } from '@noodl-core-ui/components/property-panel/PropertyPanelNumberInput';
import { PropertyPanelSelectInput } from '@noodl-core-ui/components/property-panel/PropertyPanelSelectInput';
import { PropertyPanelSliderInput } from '@noodl-core-ui/components/property-panel/PropertyPanelSliderInput';
import { PropertyPanelTextInput } from '@noodl-core-ui/components/property-panel/PropertyPanelTextInput';
import { PropertyPanelTextRadioInput } from '@noodl-core-ui/components/property-panel/PropertyPanelTextRadioInput';
import { BasePanel } from '@noodl-core-ui/components/sidebar/BasePanel';
import { CollapsableSection } from '@noodl-core-ui/components/sidebar/CollapsableSection';
import { PanelHeader } from '@noodl-core-ui/components/sidebar/PanelHeader';
import { DefaultApp } from '@noodl-core-ui/preview/template/DefaultApp';
export function Group() {
return <DefaultApp panel={<GroupPanel />} />;
}
function GroupPanel() {
return (
<BasePanel isFill>
<PanelHeader title="Group" />
<ScrollArea>
<VStack>
<CollapsableSection title="Margin and padding">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelMarginPadding
values={{
padding: { top: '10px', bottom: '10px', left: '10px', right: '10px' },
margin: { top: '10px', bottom: '10px', left: '10px', right: '10px' }
}}
/>
</Box>
</CollapsableSection>
<CollapsableSection title="Alignment">
<Box hasXSpacing>
<PropertyPanelRow isChanged={false} label="Horizontal">
<PropertyPanelIconRadioInput
value="left"
properties={{
name: '',
options: [
{
icon: <Icon icon={IconName.AlignItemLeft} />,
value: 'left'
},
{
icon: <Icon icon={IconName.AlignItemCenter} />,
value: 'center'
},
{
icon: <Icon icon={IconName.AlignItemRight} />,
value: 'right'
}
]
}}
/>
</PropertyPanelRow>
</Box>
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow isChanged={false} label="Vertical">
<PropertyPanelIconRadioInput
value="flex-end"
properties={{
name: '',
options: [
{
icon: <Icon icon={IconName.JustifyContentEnd} />,
value: 'flex-end'
},
{
icon: <Icon icon={IconName.JustifyContentCenter} />,
value: 'center'
},
{
icon: <Icon icon={IconName.JustifyContentStart} />,
value: 'flex-start'
}
]
}}
/>
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Dimensions">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelInput
label="Size mode"
inputType={PropertyPanelInputType.IconRadio}
value="flex-end"
properties={{
name: '',
options: [
{
icon: <Icon icon={IconName.DimensionWidthHeight} size={IconSize.Large} />,
value: 'flex-end'
},
{
icon: <Icon icon={IconName.DimenstionWidth} size={IconSize.Large} />,
value: 'center'
},
{
icon: <Icon icon={IconName.DimensionHeight} size={IconSize.Large} />,
value: 'flex-start'
},
{
icon: <Icon icon={IconName.Dimension} size={IconSize.Large} />,
value: 'flex-start'
}
]
}}
/>
</Box>
</CollapsableSection>
<CollapsableSection title="Layout">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Position" isChanged={false}>
<PropertyPanelSelectInput
properties={{
options: [{ label: 'In Layout', value: 'normal' }]
}}
value="normal"
/>
</PropertyPanelRow>
<PropertyPanelRow label="Direction" isChanged={false}>
<PropertyPanelTextRadioInput
onChange={() => {}}
value={'normal'}
properties={{
name: 'layout-direction',
options: [
{ label: 'Vertical', value: 'normal' },
{ label: 'Horizontal', value: 'horizontal' }
]
}}
/>
</PropertyPanelRow>
<PropertyPanelInput
label="Multi Line Wrap"
inputType={PropertyPanelInputType.TextRadio}
value="normal"
properties={{
options: [
{ label: 'Off', value: 'normal' },
{ label: 'On', value: 'on' },
{ label: 'Reverse', value: 'reverse' }
]
}}
/>
<PropertyPanelRow label="Vertical Gap" isChanged={false}>
<PropertyPanelLengthUnitInput value="0px" />
</PropertyPanelRow>
<PropertyPanelRow label="Clip Content" isChanged={false}>
<PropertyPanelCheckbox value={false} />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Align and justify content">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelIconRadioInput
value="left"
properties={{
name: '',
options: [
{
icon: <Icon icon={IconName.AlignItemLeft} />,
value: 'left'
},
{
icon: <Icon icon={IconName.AlignItemCenter} />,
value: 'center'
},
{
icon: <Icon icon={IconName.AlignItemRight} />,
value: 'right'
}
]
}}
/>
</Box>
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelIconRadioInput
value="flex-end"
properties={{
name: '',
options: [
{
icon: <Icon icon={IconName.JustifyContentEnd} />,
value: 'flex-end'
},
{
icon: <Icon icon={IconName.JustifyContentCenter} />,
value: 'center'
},
{
icon: <Icon icon={IconName.JustifyContentStart} />,
value: 'flex-start'
},
{
icon: <Icon icon={IconName.JustifyContentSpaceBetween} />,
value: 'flex-start'
},
{
icon: <Icon icon={IconName.JustifyContentSpaceAround} />,
value: 'flex-start'
},
{
icon: <Icon icon={IconName.JustifyContentSpaceEvenly} />,
value: 'flex-start'
}
]
}}
/>
</Box>
</CollapsableSection>
<CollapsableSection title="Scroll">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Enable Scroll" isChanged={false}>
<PropertyPanelCheckbox value={false} />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Style">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Opacity" isChanged={false}>
<PropertyPanelSliderInput value={1} properties={{ min: 0, max: 1 }} />
</PropertyPanelRow>
<PropertyPanelRow label="Blend Mode" isChanged={false}>
<PropertyPanelSelectInput
properties={{
options: [{ label: 'Normal', value: 'normal' }]
}}
value="normal"
/>
</PropertyPanelRow>
<PropertyPanelRow label="Background Color" isChanged={false}>
{/* TODO: Color */}
<PropertyPanelLengthUnitInput value="0px" />
</PropertyPanelRow>
<PropertyPanelRow label="Visible" isChanged={false}>
<PropertyPanelCheckbox value={true} />
</PropertyPanelRow>
<PropertyPanelRow label="zIndex" isChanged={false}>
<PropertyPanelNumberInput value="" />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Border Style">
<Box hasXSpacing>
<PropertyPanelInput
label="Editing border"
inputType={PropertyPanelInputType.IconRadio}
value="left"
properties={{
name: '',
options: [
{
icon: <Icon icon={IconName.BorderAll} />,
value: 'left'
},
{
icon: <Icon icon={IconName.BorderLeft} />,
value: 'center'
},
{
icon: <Icon icon={IconName.BorderUp} />,
value: 'right'
},
{
icon: <Icon icon={IconName.BorderRight} />,
value: 'right'
},
{
icon: <Icon icon={IconName.BorderDown} />,
value: 'right'
}
]
}}
/>
</Box>
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Border Style" isChanged={false}>
<PropertyPanelSelectInput
properties={{
options: [
{ label: 'None', value: 'none' },
{ label: 'Solid', value: 'solid' },
{ label: 'Dotted', value: 'dotted' },
{ label: 'Dashed', value: 'dashed' }
]
}}
value="none"
/>
</PropertyPanelRow>
<PropertyPanelRow label="Border Width" isChanged={false}>
<PropertyPanelLengthUnitInput value="0px" />
</PropertyPanelRow>
<PropertyPanelRow label="Border Color" isChanged={false}>
{/* TODO: Color */}
<PropertyPanelLengthUnitInput value="0px" />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Corner Radius">
<Box hasXSpacing>
<PropertyPanelInput
label="Editing corner"
inputType={PropertyPanelInputType.IconRadio}
value="left"
properties={{
name: '',
options: [
{
icon: <Icon icon={IconName.RoundedCornerAll} />,
value: 'left'
},
{
icon: <Icon icon={IconName.RoundedCornerLeftUp} />,
value: 'center'
},
{
icon: <Icon icon={IconName.RoundedCornerRightUp} />,
value: 'right'
},
{
icon: <Icon icon={IconName.RoundedCornerRightDown} />,
value: 'right'
},
{
icon: <Icon icon={IconName.RoundedCornerLeftDown} />,
value: 'right'
}
]
}}
/>
</Box>
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Corner Radius" isChanged={false}>
<PropertyPanelLengthUnitInput value="0px" />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Box Shadow">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Shadow Enabled" isChanged={false}>
<PropertyPanelCheckbox value={false} />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Placement">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Pos X" isChanged={false}>
<PropertyPanelLengthUnitInput value="0px" />
</PropertyPanelRow>
<PropertyPanelRow label="Pos Y" isChanged={false}>
<PropertyPanelLengthUnitInput value="0px" />
</PropertyPanelRow>
<PropertyPanelInput
inputType={PropertyPanelInputType.Slider}
label="Rotation"
properties={{ min: -365, max: 365 }}
value={0}
/>
<PropertyPanelRow label="Scale" isChanged={false}>
<PropertyPanelNumberInput value="1" />
</PropertyPanelRow>
<PropertyPanelRow label="Transform Origin X" isChanged={false}>
<PropertyPanelLengthUnitInput value="50%" />
</PropertyPanelRow>
<PropertyPanelRow label="Transform Origin Y" isChanged={false}>
<PropertyPanelLengthUnitInput value="50%" />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Dimension Constraints">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Min Width" isChanged={false}>
<PropertyPanelLengthUnitInput value="auto" />
</PropertyPanelRow>
<PropertyPanelRow label="Max Width" isChanged={false}>
<PropertyPanelLengthUnitInput value="auto" />
</PropertyPanelRow>
<PropertyPanelRow label="Min Height" isChanged={false}>
<PropertyPanelLengthUnitInput value="auto" />
</PropertyPanelRow>
<PropertyPanelRow label="Max Height" isChanged={false}>
<PropertyPanelLengthUnitInput value="auto" />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Pointer Events">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelInput
label="Mode"
inputType={PropertyPanelInputType.TextRadio}
value="inherit"
properties={{
options: [
{ label: 'Inherit', value: 'inherit' },
{ label: 'Explicit', value: 'explicit' }
]
}}
/>
<PropertyPanelRow label="Block events" isChanged={false}>
<PropertyPanelCheckbox value={true} />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="General">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="Mounted" isChanged={false}>
<PropertyPanelCheckbox value={true} />
</PropertyPanelRow>
</Box>
</CollapsableSection>
<CollapsableSection title="Advanced Style">
<Box hasXSpacing hasBottomSpacing>
<PropertyPanelRow label="CSS Class" isChanged={false}>
<PropertyPanelTextInput value="" />
</PropertyPanelRow>
<PropertyPanelRow label="CSS Style" isChanged={false}>
<PropertyPanelButton
properties={{
buttonLabel: 'Edit'
}}
/>
</PropertyPanelRow>
</Box>
</CollapsableSection>
</VStack>
</ScrollArea>
</BasePanel>
);
}

View File

@@ -0,0 +1,33 @@
.Root {
--title-bar-height: 30px;
background-color: var(--theme-color-bg-2);
display: grid;
grid-template-rows: var(--title-bar-height) auto;
border-radius: 3px;
overflow: hidden;
width: 1000px;
height: 720px;
}
.TitleBar {
grid-row: 1;
border-bottom: 2px solid var(--theme-color-bg-1);
display: flex;
justify-content: center;
align-items: center;
color: var(--theme-color-fg-default);
font-family: var(--font-family);
font-weight: var(--font-weight-regular);
font-size: 12px;
user-select: none;
}
.Main {
grid-row: 2;
}

View File

@@ -0,0 +1,17 @@
import React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { DefaultApp } from "./DefaultApp";
export default {
title: "Preview/Template/App",
component: DefaultApp,
argTypes: {},
} as ComponentMeta<typeof DefaultApp>;
const Template: ComponentStory<typeof DefaultApp> = (args) => (
<DefaultApp {...args}></DefaultApp>
);
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,63 @@
import classNames from 'classnames';
import React from 'react';
import { SideNavigation } from '@noodl-core-ui/components/app/SideNavigation';
import { TitleBar } from '@noodl-core-ui/components/app/TitleBar';
import { FrameDivider } from '@noodl-core-ui/components/layout/FrameDivider';
import { Slot } from '@noodl-core-ui/types/global';
import css from './DefaultApp.module.scss';
/**
* Returns whether we are running inside the storybook editor canvas.
*
* @returns
*/
export function insideFrame() {
// // The page is in an iframe
return window.location !== window.parent.location;
}
export interface DefaultAppProps {
title?: string;
panel?: JSX.Element;
document?: Slot;
}
export function DefaultApp({ title = 'Noodl Storybook', panel, document }: DefaultAppProps) {
const [frameSize, setFrameSize] = React.useState(320);
const size = insideFrame()
? {
width: 1280,
height: 1000
}
: {
width: 1440,
height: 1024
};
return (
<div
className={classNames([css['Root']])}
style={{
...size,
position: 'relative'
}}
>
<TitleBar title={title} version="2.7.0" isWindows />
<div className={classNames([css['Main']])}>
<FrameDivider
size={frameSize}
onSizeChanged={setFrameSize}
sizeMin={300}
sizeMax={800}
first={<SideNavigation toolbar={<></>} panel={panel} />}
second={document}
horizontal
/>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './DefaultApp';

View File

@@ -0,0 +1,42 @@
.Root {
--title-bar-height: 30px;
display: grid;
grid-template-columns: 280px auto;
grid-template-rows: var(--title-bar-height) auto;
border-radius: 3px;
overflow: hidden;
width: 1000px;
height: 720px;
background-color: var(--theme-color-bg-1);
}
.TitleBar {
grid-column: 1 / -1;
grid-row: 1;
display: flex;
justify-content: center;
align-items: center;
color: var(--theme-color-fg-default);
font-family: var(--font-family);
font-weight: var(--font-weight-regular);
font-size: 12px;
border-bottom: 2px solid var(--theme-color-bg-1);
background-color: var(--theme-color-bg-2);
user-select: none;
}
.SidePanel {
grid-column: 1;
grid-row: 2;
border-right: 2px solid var(--theme-color-bg-1);
background-color: var(--theme-color-bg-2);
}

View File

@@ -0,0 +1,22 @@
import React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { LauncherApp, LauncherSidebarExample } from "./LauncherApp";
export default {
title: "Preview/Template/Launcher",
component: LauncherApp,
argTypes: {},
} as ComponentMeta<typeof LauncherApp>;
const Template: ComponentStory<typeof LauncherApp> = (args) => (
<LauncherApp {...args}></LauncherApp>
);
export const Common = Template.bind({});
Common.args = {};
export const WithSidebar = Template.bind({});
WithSidebar.args = {
sidePanel: <LauncherSidebarExample />
};

View File

@@ -0,0 +1,165 @@
import classNames from 'classnames';
import React from 'react';
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
import { Logo } from '@noodl-core-ui/components/common/Logo';
import { ExternalLink } from '@noodl-core-ui/components/inputs/ExternalLink';
import { IconButton, IconButtonVariant } from '@noodl-core-ui/components/inputs/IconButton';
import { Container, ContainerDirection } from '@noodl-core-ui/components/layout/Container';
import { ListItem } from '@noodl-core-ui/components/layout/ListItem';
import { Section } from '@noodl-core-ui/components/sidebar/Section';
import { Text } from '@noodl-core-ui/components/typography/Text';
import { Title, TitleSize, TitleVariant } from '@noodl-core-ui/components/typography/Title';
import { UserBadge, UserBadgeSize } from '@noodl-core-ui/components/user/UserBadge';
import { UserListingCard, UserListingCardVariant } from '@noodl-core-ui/components/user/UserListingCard';
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
import css from './LauncherApp.module.scss';
/**
* Returns whether we are running inside the storybook editor canvas.
*
* @returns
*/
export function insideFrame() {
// // The page is in an iframe
return window.location !== window.parent.location;
}
export interface LauncherSectionProps extends UnsafeStyleProps {
hasTopBorder?: boolean;
children: JSX.Element | JSX.Element[];
}
export function LauncherSection({ hasTopBorder, children, UNSAFE_style, UNSAFE_className }: LauncherSectionProps) {
return (
<div
style={{
width: '100%',
borderTop: hasTopBorder ? '1px solid var(--theme-color-bg-3)' : null,
padding: '30px',
boxSizing: 'border-box',
...UNSAFE_style
}}
className={UNSAFE_className}
>
{children}
</div>
);
}
export function LauncherSidebarExample() {
return (
<Container direction={ContainerDirection.Vertical} hasSpaceBetween>
<Container direction={ContainerDirection.Vertical}>
<LauncherSection>
<Logo />
</LauncherSection>
<Container hasXSpacing>
<Container direction={ContainerDirection.Horizontal} hasXSpacing>
<Container direction={ContainerDirection.Vertical}>
<Text>Workspace</Text>
<Title variant={TitleVariant.Highlighted} size={TitleSize.Large} hasBottomSpacing>
Noodl tutorials
</Title>
</Container>
<Icon icon={IconName.CaretDown} />
</Container>
</Container>
<LauncherSection>
<Text>Workspace</Text>
<Title variant={TitleVariant.Highlighted} size={TitleSize.Large} hasBottomSpacing>
Noodl tutorials
</Title>
</LauncherSection>
<Section>
<Container hasXSpacing hasYSpacing>
<Container hasXSpacing hasTopSpacing>
<UserBadge email="john@noodl.net" id="20" name="John Doe" size={UserBadgeSize.Small} hasRightSpacing />
<UserBadge email="john@noodl.net" id="20" name="John Doe" size={UserBadgeSize.Small} hasRightSpacing />
<UserBadge email="john@noodl.net" id="20" name="John Doe" size={UserBadgeSize.Small} hasRightSpacing />
<UserBadge email="john@noodl.net" id="20" name="John Doe" size={UserBadgeSize.Small} hasRightSpacing />
<UserBadge email="john@noodl.net" id="20" name="John Doe" size={UserBadgeSize.Small} hasRightSpacing />
<IconButton
icon={IconName.DotsThreeHorizontal}
variant={IconButtonVariant.Transparent}
size={IconSize.Small}
/>
</Container>
</Container>
<Container direction={ContainerDirection.Vertical} hasBottomSpacing>
<ListItem gutter={2} text="Invite" icon={IconName.Plus} />
</Container>
</Section>
<Section>
<Container direction={ContainerDirection.Vertical} hasYSpacing>
<ListItem gutter={2} text="Learn" icon={IconName.Palette} />
<ListItem gutter={2} text="Projects" icon={IconName.Components} />
</Container>
</Section>
{/*
<Section>
<Container direction={ContainerDirection.Vertical} hasYSpacing>
<ListItem gutter={2} text="Recent projects" />
<ListItem gutter={2} text="Project A" />
<ListItem gutter={2} text="Project B" />
<ListItem gutter={2} text="Project C" />
</Container>
</Section>
*/}
<Section>
<Container direction={ContainerDirection.Vertical} hasYSpacing>
<ListItem gutter={2} text="Documentation" icon={IconName.File} />
<ListItem gutter={2} text="Community" icon={IconName.Chat} />
</Container>
</Section>
</Container>
<Section>
<Container direction={ContainerDirection.Vertical} hasXSpacing hasYSpacing>
<UserListingCard variant={UserListingCardVariant.Launcher} email="john@noodl.net" id="20" name="John Doe" />
</Container>
</Section>
</Container>
);
}
export interface LauncherAppProps {
title?: string;
sidePanel?: JSX.Element;
children?: JSX.Element | JSX.Element[];
}
export function LauncherApp({ title = 'Noodl Launcher', sidePanel, children }: LauncherAppProps) {
const size = insideFrame()
? {
width: 1280,
height: 1000
}
: {
width: 1440,
height: 1024
};
return (
<div
className={classNames([css['Root']])}
style={{
...size
}}
>
<div className={classNames([css['TitleBar']])}>{title}</div>
<div className={classNames([css['SidePanel']])}>{sidePanel}</div>
<div>{children}</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './LauncherApp';