mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-13 07:42:55 +01:00
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:
@@ -0,0 +1,58 @@
|
||||
.ScrollContainer {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ScrollArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// padding-top: 8px;
|
||||
// padding-bottom: 8px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
.JumpToPresentContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 8px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.JumpToPresent {
|
||||
display: none;
|
||||
|
||||
padding: 4px;
|
||||
background-color: var(--theme-color-bg-2);
|
||||
border: 1px solid var(--theme-color-bg-3);
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
pointer-events: all;
|
||||
|
||||
transition: background-color 100ms ease;
|
||||
transform: translate(0, 20%);
|
||||
animation: slide 500ms forwards;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
|
||||
&.is-past {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { AiChatLoader } from '@noodl-core-ui/components/ai/AiChatLoader';
|
||||
import { AiChatMessage, AiChatMessageProps } from '@noodl-core-ui/components/ai/AiChatMessage';
|
||||
import { AiChatSuggestion } from '@noodl-core-ui/components/ai/AiChatSuggestion';
|
||||
import { SideNavigation, SideNavigationButton } from '@noodl-core-ui/components/app/SideNavigation';
|
||||
import { IconName } from '@noodl-core-ui/components/common/Icon';
|
||||
import { PrimaryButton, PrimaryButtonSize, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
|
||||
import { TextArea } from '@noodl-core-ui/components/inputs/TextArea';
|
||||
import { Box } from '@noodl-core-ui/components/layout/Box';
|
||||
import { Container, ContainerDirection } from '@noodl-core-ui/components/layout/Container';
|
||||
import { VStack } from '@noodl-core-ui/components/layout/Stack';
|
||||
|
||||
import { AiChatBox } from './AiChatBox';
|
||||
|
||||
export default {
|
||||
title: 'Ai/Ai ChatBox',
|
||||
component: AiChatBox,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof AiChatBox>;
|
||||
|
||||
export const Preview = () => (
|
||||
<div style={{ maxWidth: '380px', height: '800px' }}>
|
||||
<SideNavigation
|
||||
toolbar={
|
||||
<>
|
||||
<Container direction={ContainerDirection.Vertical} UNSAFE_style={{ flex: '1' }}>
|
||||
<SideNavigationButton icon={IconName.Components} label={'Components'} />
|
||||
<SideNavigationButton icon={IconName.Search} label={'Search'} />
|
||||
<SideNavigationButton icon={IconName.Collaboration} label={'Collaboration'} />
|
||||
<SideNavigationButton icon={IconName.StructureCircle} label={'Version control'} />
|
||||
<SideNavigationButton icon={IconName.CloudData} label={'Cloud Services'} />
|
||||
<SideNavigationButton icon={IconName.CloudFunction} label={'Cloud functions'} />
|
||||
<SideNavigationButton icon={IconName.Setting} label={'Project settings'} />
|
||||
</Container>
|
||||
<Container direction={ContainerDirection.Vertical}>
|
||||
<SideNavigationButton icon={IconName.SlidersHorizontal} label={'Editor settings'} />
|
||||
</Container>
|
||||
</>
|
||||
}
|
||||
panel={
|
||||
<AiChatBox
|
||||
footer={
|
||||
<>
|
||||
<TextArea value="Message" hasBottomSpacing isResizeDisabled UNSAFE_style={{ minHeight: '72px' }} />
|
||||
<VStack hasSpacing={1}>
|
||||
<PrimaryButton label="Send Message" size={PrimaryButtonSize.Small} isGrowing />
|
||||
</VStack>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<AiChatMessage user={{ role: 'user', name: 'Tore K' }} content="Get the current weather at my location." />
|
||||
|
||||
<AiChatMessage
|
||||
user={{ role: 'assistant' }}
|
||||
content="This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API. It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals."
|
||||
/>
|
||||
|
||||
<Box hasYSpacing={2} UNSAFE_style={{ borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
</Box>
|
||||
</AiChatBox>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const WithAffix = () => (
|
||||
<div style={{ maxWidth: '380px', height: '800px' }}>
|
||||
<SideNavigation
|
||||
toolbar={
|
||||
<>
|
||||
<Container direction={ContainerDirection.Vertical} UNSAFE_style={{ flex: '1' }}>
|
||||
<SideNavigationButton icon={IconName.Components} label={'Components'} />
|
||||
<SideNavigationButton icon={IconName.Search} label={'Search'} />
|
||||
<SideNavigationButton icon={IconName.Collaboration} label={'Collaboration'} />
|
||||
<SideNavigationButton icon={IconName.StructureCircle} label={'Version control'} />
|
||||
<SideNavigationButton icon={IconName.CloudData} label={'Cloud Services'} />
|
||||
<SideNavigationButton icon={IconName.CloudFunction} label={'Cloud functions'} />
|
||||
<SideNavigationButton icon={IconName.Setting} label={'Project settings'} />
|
||||
</Container>
|
||||
<Container direction={ContainerDirection.Vertical}>
|
||||
<SideNavigationButton icon={IconName.SlidersHorizontal} label={'Editor settings'} />
|
||||
</Container>
|
||||
</>
|
||||
}
|
||||
panel={
|
||||
<AiChatBox
|
||||
footer={
|
||||
<>
|
||||
<TextArea value="Message" hasBottomSpacing isResizeDisabled UNSAFE_style={{ minHeight: '72px' }} />
|
||||
<VStack hasSpacing={1}>
|
||||
<PrimaryButton label="Send Message" isGrowing />
|
||||
</VStack>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<AiChatMessage user={{ role: 'user', name: 'Tore K' }} content="Get the current weather at my location." />
|
||||
|
||||
<AiChatMessage
|
||||
user={{ role: 'assistant' }}
|
||||
content="This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API. It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals."
|
||||
affix={
|
||||
<PrimaryButton
|
||||
size={PrimaryButtonSize.Small}
|
||||
variant={PrimaryButtonVariant.MutedOnLowBg}
|
||||
icon={IconName.ImportSlanted}
|
||||
label="Open code editor"
|
||||
isGrowing
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Box hasYSpacing={2} UNSAFE_style={{ borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
</Box>
|
||||
</AiChatBox>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const DifferentStates = () => (
|
||||
<div style={{ maxWidth: '380px', height: '800px' }}>
|
||||
<SideNavigation
|
||||
toolbar={
|
||||
<>
|
||||
<Container direction={ContainerDirection.Vertical} UNSAFE_style={{ flex: '1' }}>
|
||||
<SideNavigationButton icon={IconName.Components} label={'Components'} />
|
||||
<SideNavigationButton icon={IconName.Search} label={'Search'} />
|
||||
<SideNavigationButton icon={IconName.Collaboration} label={'Collaboration'} />
|
||||
<SideNavigationButton icon={IconName.StructureCircle} label={'Version control'} />
|
||||
<SideNavigationButton icon={IconName.CloudData} label={'Cloud Services'} />
|
||||
<SideNavigationButton icon={IconName.CloudFunction} label={'Cloud functions'} />
|
||||
<SideNavigationButton icon={IconName.Setting} label={'Project settings'} />
|
||||
</Container>
|
||||
<Container direction={ContainerDirection.Vertical}>
|
||||
<SideNavigationButton icon={IconName.SlidersHorizontal} label={'Editor settings'} />
|
||||
</Container>
|
||||
</>
|
||||
}
|
||||
panel={
|
||||
<AiChatBox
|
||||
footer={
|
||||
<>
|
||||
<TextArea value="Message" hasBottomSpacing isResizeDisabled UNSAFE_style={{ minHeight: '72px' }} />
|
||||
<VStack hasSpacing={1}>
|
||||
<PrimaryButton label="Send Message" isGrowing />
|
||||
</VStack>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box hasYSpacing={2} UNSAFE_style={{ borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<AiChatSuggestion text="Empty states" />
|
||||
</Box>
|
||||
<AiChatMessage user={{ role: 'user', name: 'Tore K' }} content="" />
|
||||
<AiChatMessage user={{ role: 'assistant' }} content="" />
|
||||
|
||||
<Box hasYSpacing={2} UNSAFE_style={{ borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<AiChatSuggestion text="One-liners" />
|
||||
</Box>
|
||||
<AiChatMessage user={{ role: 'user', name: 'Tore K' }} content="One-liner" />
|
||||
<AiChatMessage user={{ role: 'assistant' }} content="One-liner" />
|
||||
|
||||
<Box hasYSpacing={2} UNSAFE_style={{ borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<AiChatSuggestion text="Multi-liners" />
|
||||
</Box>
|
||||
<AiChatMessage user={{ role: 'user', name: 'Tore K' }} content={`Line 1\nLine 2`} />
|
||||
<AiChatMessage user={{ role: 'assistant' }} content={`Line 1\nLine 2`} />
|
||||
|
||||
<Box hasYSpacing={2} UNSAFE_style={{ borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<AiChatSuggestion text="Wrapping Text" />
|
||||
</Box>
|
||||
<AiChatMessage
|
||||
user={{ role: 'user', name: 'Tore K' }}
|
||||
content={`This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API. It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals.`}
|
||||
/>
|
||||
<AiChatMessage
|
||||
user={{ role: 'assistant' }}
|
||||
content={`This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API. It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals.`}
|
||||
/>
|
||||
|
||||
<AiChatLoader />
|
||||
</AiChatBox>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
type LiveItem = {
|
||||
chat: AiChatMessageProps;
|
||||
};
|
||||
|
||||
const UserLiveItem: LiveItem = {
|
||||
chat: {
|
||||
user: { role: 'user', name: 'Tore K' },
|
||||
content: 'Get the current weather at my location.'
|
||||
}
|
||||
};
|
||||
|
||||
const AssistantLiveItem: LiveItem = {
|
||||
chat: {
|
||||
user: { role: 'assistant' },
|
||||
content:
|
||||
"This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API.\n\n It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals."
|
||||
}
|
||||
};
|
||||
|
||||
export const LiveText = () => {
|
||||
const [messages, setMessages] = useState<LiveItem[]>([
|
||||
{
|
||||
chat: {
|
||||
user: { role: 'user', name: 'Tore K' },
|
||||
content: 'Get the current weather at my location.'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const func = () => {
|
||||
setMessages((prev) => {
|
||||
const last = prev.length ? prev[prev.length - 1] : null;
|
||||
if (last?.chat?.user?.role !== 'assistant') {
|
||||
return [...prev, AssistantLiveItem];
|
||||
} else {
|
||||
return [...prev, UserLiveItem];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const id = setInterval(func, 1000);
|
||||
return function () {
|
||||
clearInterval(id);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '380px', height: '800px' }}>
|
||||
<SideNavigation
|
||||
toolbar={
|
||||
<>
|
||||
<Container direction={ContainerDirection.Vertical} UNSAFE_style={{ flex: '1' }}>
|
||||
<SideNavigationButton icon={IconName.Components} label={'Components'} />
|
||||
<SideNavigationButton icon={IconName.Search} label={'Search'} />
|
||||
<SideNavigationButton icon={IconName.Collaboration} label={'Collaboration'} />
|
||||
<SideNavigationButton icon={IconName.StructureCircle} label={'Version control'} />
|
||||
<SideNavigationButton icon={IconName.CloudData} label={'Cloud Services'} />
|
||||
<SideNavigationButton icon={IconName.CloudFunction} label={'Cloud functions'} />
|
||||
<SideNavigationButton icon={IconName.Setting} label={'Project settings'} />
|
||||
</Container>
|
||||
<Container direction={ContainerDirection.Vertical}>
|
||||
<SideNavigationButton icon={IconName.SlidersHorizontal} label={'Editor settings'} />
|
||||
</Container>
|
||||
</>
|
||||
}
|
||||
panel={
|
||||
<AiChatBox
|
||||
footer={
|
||||
<>
|
||||
<TextArea value="Message" hasBottomSpacing isResizeDisabled UNSAFE_style={{ minHeight: '72px' }} />
|
||||
<VStack hasSpacing={1}>
|
||||
<PrimaryButton label="Send Message" size={PrimaryButtonSize.Small} isGrowing />
|
||||
</VStack>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{messages.map((x, i) => (
|
||||
<AiChatMessage key={i} {...x.chat} />
|
||||
))}
|
||||
|
||||
{messages.length && messages[messages.length - 1]?.chat?.user?.role === 'assistant' && (
|
||||
<Box hasYSpacing={2} UNSAFE_style={{ borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
</Box>
|
||||
)}
|
||||
</AiChatBox>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LongChatLog = () => {
|
||||
const [messages] = useState<LiveItem[]>(
|
||||
[...new Array(101)].map((_, index) => (index % 2 === 0 ? AssistantLiveItem : UserLiveItem))
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '380px', height: '800px' }}>
|
||||
<SideNavigation
|
||||
toolbar={
|
||||
<>
|
||||
<Container direction={ContainerDirection.Vertical} UNSAFE_style={{ flex: '1' }}>
|
||||
<SideNavigationButton icon={IconName.Components} label={'Components'} />
|
||||
<SideNavigationButton icon={IconName.Search} label={'Search'} />
|
||||
<SideNavigationButton icon={IconName.Collaboration} label={'Collaboration'} />
|
||||
<SideNavigationButton icon={IconName.StructureCircle} label={'Version control'} />
|
||||
<SideNavigationButton icon={IconName.CloudData} label={'Cloud Services'} />
|
||||
<SideNavigationButton icon={IconName.CloudFunction} label={'Cloud functions'} />
|
||||
<SideNavigationButton icon={IconName.Setting} label={'Project settings'} />
|
||||
</Container>
|
||||
<Container direction={ContainerDirection.Vertical}>
|
||||
<SideNavigationButton icon={IconName.SlidersHorizontal} label={'Editor settings'} />
|
||||
</Container>
|
||||
</>
|
||||
}
|
||||
panel={
|
||||
<AiChatBox
|
||||
footer={
|
||||
<>
|
||||
<TextArea value="Message" hasBottomSpacing isResizeDisabled UNSAFE_style={{ minHeight: '72px' }} />
|
||||
<VStack hasSpacing={1}>
|
||||
<PrimaryButton label="Send Message" size={PrimaryButtonSize.Small} isGrowing />
|
||||
</VStack>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{messages.map((x, i) => (
|
||||
<AiChatMessage key={i} {...x.chat} />
|
||||
))}
|
||||
|
||||
{messages.length && messages[messages.length - 1]?.chat?.user?.role === 'assistant' && (
|
||||
<Box hasYSpacing={2} UNSAFE_style={{ borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
<AiChatSuggestion text="What are the required inputs for this node to work correctly?" />
|
||||
</Box>
|
||||
)}
|
||||
</AiChatBox>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
121
packages/noodl-core-ui/src/components/ai/AiChatBox/AiChatBox.tsx
Normal file
121
packages/noodl-core-ui/src/components/ai/AiChatBox/AiChatBox.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
|
||||
import { HStack, VStack } from '@noodl-core-ui/components/layout/Stack';
|
||||
import { Section } from '@noodl-core-ui/components/sidebar/Section';
|
||||
import { Text } from '@noodl-core-ui/components/typography/Text';
|
||||
import { Slot } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './AiChatBox.module.scss';
|
||||
|
||||
export interface AiChatBoxProps {
|
||||
children: Slot;
|
||||
footer: Slot;
|
||||
}
|
||||
|
||||
export function AiChatBox({ children, footer }: AiChatBoxProps) {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const scrollBottomRef = useRef<HTMLSpanElement>(null);
|
||||
const [isTracking, setIsTracking] = useState(true);
|
||||
const isTrackingRef = useRef(true);
|
||||
const lastScrollTop = useRef(0);
|
||||
const isJumpingToPresent = useRef(false);
|
||||
|
||||
const onResize = useCallback(() => {
|
||||
if (isTrackingRef.current && scrollBottomRef.current) {
|
||||
scrollBottomRef.current.scrollIntoView({ behavior: 'auto' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollableRef.current) return;
|
||||
|
||||
function handleScroll() {
|
||||
const elem = scrollableRef.current;
|
||||
|
||||
if (!isJumpingToPresent.current) {
|
||||
// Remove decimals from scrollTop to make it pixel perfect
|
||||
elem.scrollTop = Math.round(elem.scrollTop);
|
||||
}
|
||||
|
||||
// scrollTop can be half a pixel off, so you will never hit the bottom.
|
||||
const isAtBottom = elem.scrollTop + elem.clientHeight + 1 >= elem.scrollHeight;
|
||||
setIsTracking(isAtBottom);
|
||||
isTrackingRef.current = isAtBottom;
|
||||
|
||||
lastScrollTop.current = scrollableRef.current.scrollTop;
|
||||
|
||||
if (isAtBottom && isJumpingToPresent.current) {
|
||||
isJumpingToPresent.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
const scrollable = scrollableRef.current;
|
||||
scrollable.addEventListener('scroll', handleScroll);
|
||||
|
||||
const observer = new ResizeObserver(onResize);
|
||||
observer.observe(scrollableRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
scrollable.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [onResize, scrollableRef]);
|
||||
|
||||
useEffect(() => {
|
||||
// Scroll to the bottom at the start
|
||||
if (scrollBottomRef.current) {
|
||||
scrollBottomRef.current.scrollIntoView({ behavior: 'auto' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollableRef.current && !isTracking) {
|
||||
scrollableRef.current.scrollTop = lastScrollTop.current;
|
||||
}
|
||||
}, [children, isTracking]);
|
||||
|
||||
function handleScrollToBottom() {
|
||||
if (scrollBottomRef.current) {
|
||||
isJumpingToPresent.current = true;
|
||||
scrollBottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
if (isTracking && scrollBottomRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollBottomRef.current) {
|
||||
scrollBottomRef.current.scrollIntoView({ behavior: 'auto' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack UNSAFE_style={{ height: '100%', width: '100%' }}>
|
||||
<div className={css['ScrollContainer']}>
|
||||
<div className={css['ScrollArea']} ref={scrollableRef}>
|
||||
{children}
|
||||
<span ref={scrollBottomRef}></span>
|
||||
</div>
|
||||
|
||||
{!isTracking && (
|
||||
<div className={css['JumpToPresentContainer']}>
|
||||
<div className={classNames(css['JumpToPresent'], css['is-past'])} onClick={handleScrollToBottom}>
|
||||
<HStack hasSpacing={1}>
|
||||
<Text>Jump To Present</Text>
|
||||
<Icon icon={IconName.ArrowDown} size={IconSize.Tiny} />
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{Boolean(footer) && (
|
||||
<Section hasGutter hasTopDivider hasVisibleOverflow>
|
||||
{footer}
|
||||
</Section>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AiChatBox';
|
||||
@@ -0,0 +1,20 @@
|
||||
.Root {
|
||||
border: 1px solid var(--theme-color-bg-2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.Container {
|
||||
padding: 12px 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.Mounted {
|
||||
transform: translateY(0px);
|
||||
opacity: 1;
|
||||
transition: background-color 100ms ease, opacity 500ms ease, transform 0.5s ease;
|
||||
}
|
||||
|
||||
.Mounting {
|
||||
transform: translateY(8px);
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AiChatCard } from './AiChatCard';
|
||||
|
||||
export default {
|
||||
title: 'Ai/Ai Chat Card',
|
||||
component: AiChatCard,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof AiChatCard>;
|
||||
|
||||
const Template: ComponentStory<typeof AiChatCard> = (args) => <AiChatCard {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {
|
||||
title: 'Home page',
|
||||
subtitle: 'Landing page for the app'
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Box } from '@noodl-core-ui/components/layout/Box';
|
||||
import { Label, LabelSize } from '@noodl-core-ui/components/typography/Label';
|
||||
import { Text } from '@noodl-core-ui/components/typography/Text';
|
||||
import { Slot } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './AiChatCard.module.scss';
|
||||
|
||||
export interface AiChatCardProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children?: Slot;
|
||||
}
|
||||
|
||||
export function AiChatCard({ title, subtitle, children }: AiChatCardProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box hasXSpacing hasYSpacing={1}>
|
||||
<div className={classNames([css['Root'], mounted ? css['Mounted'] : css['Mounting']])}>
|
||||
<div className={css['Container']}>
|
||||
<Label size={LabelSize.Big} hasBottomSpacing>
|
||||
{title}
|
||||
</Label>
|
||||
{subtitle && <Text>{subtitle}</Text>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AiChatCard';
|
||||
@@ -0,0 +1,4 @@
|
||||
.Root {
|
||||
border-top: 1px solid var(--theme-color-bg-1);
|
||||
background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AiChatLoader } from './AiChatLoader';
|
||||
|
||||
export default {
|
||||
title: 'Ai/Ai Chat Loader',
|
||||
component: AiChatLoader,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof AiChatLoader>;
|
||||
|
||||
const Template: ComponentStory<typeof AiChatLoader> = (args) => (
|
||||
<div style={{ width: '337px' }}>
|
||||
<AiChatLoader {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
|
||||
export const LongText = Template.bind({});
|
||||
LongText.args = {
|
||||
text: 'Making sense of the universe... one moment please!'
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AiIconAnimated } from '@noodl-core-ui/components/ai/AiIconAnimated';
|
||||
import { Box } from '@noodl-core-ui/components/layout/Box';
|
||||
import { HStack } from '@noodl-core-ui/components/layout/Stack';
|
||||
import { Text } from '@noodl-core-ui/components/typography/Text';
|
||||
|
||||
import css from './AiChatLoader.module.scss';
|
||||
|
||||
export interface AiChatLoaderProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export function AiChatLoader({ text = 'Thinking...' }: AiChatLoaderProps) {
|
||||
return (
|
||||
<Box hasXSpacing hasYSpacing={1} UNSAFE_className={css['Root']}>
|
||||
<HStack UNSAFE_style={{ minHeight: '18px' }}>
|
||||
<AiIconAnimated isListening UNSAFE_style={{ marginLeft: '-13px', marginRight: '5px' }} />
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Text>{text}</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AiChatLoader';
|
||||
@@ -0,0 +1,3 @@
|
||||
.Root {
|
||||
border-top: 1px solid var(--theme-color-bg-1);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { IconName } from '@noodl-core-ui/components/common/Icon';
|
||||
import { PrimaryButton, PrimaryButtonSize, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
|
||||
|
||||
import { AiChatMessage } from './AiChatMessage';
|
||||
|
||||
export default {
|
||||
title: 'Ai/Ai Chat Message',
|
||||
component: AiChatMessage,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof AiChatMessage>;
|
||||
|
||||
const Template: ComponentStory<typeof AiChatMessage> = (args) => (
|
||||
<div style={{ maxWidth: '280px' }}>
|
||||
<AiChatMessage {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {
|
||||
user: {
|
||||
role: 'user',
|
||||
name: 'Tore K'
|
||||
},
|
||||
content: 'Get the current weather at my location.'
|
||||
};
|
||||
|
||||
export const User_BigContent = Template.bind({});
|
||||
User_BigContent.args = {
|
||||
user: {
|
||||
role: 'user',
|
||||
name: 'Tore K'
|
||||
},
|
||||
content: `This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API. It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals.`
|
||||
};
|
||||
|
||||
export const Assistant_BigContent = Template.bind({});
|
||||
Assistant_BigContent.args = {
|
||||
user: {
|
||||
role: 'assistant'
|
||||
},
|
||||
content: `This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API. It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals.`
|
||||
};
|
||||
|
||||
export const Assistant_BigContentAffix = Template.bind({});
|
||||
Assistant_BigContentAffix.args = {
|
||||
user: {
|
||||
role: 'assistant'
|
||||
},
|
||||
content: `This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API. It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals.`,
|
||||
affix: (
|
||||
<PrimaryButton
|
||||
size={PrimaryButtonSize.Small}
|
||||
variant={PrimaryButtonVariant.MutedOnLowBg}
|
||||
icon={IconName.ImportSlanted}
|
||||
label="Open code editor"
|
||||
isGrowing
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const None_BigContent = Template.bind({});
|
||||
None_BigContent.args = {
|
||||
user: null,
|
||||
content: `This Function node fetches a location's address using its latitude and longitude from Google's Geocoding API. It requires an API key, latitude, and longitude as inputs and outputs the formatted address and success or failure signals.`
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AiIcon } from '@noodl-core-ui/components/ai/AiIcon';
|
||||
import { Markdown } from '@noodl-core-ui/components/common/Markdown';
|
||||
import { Box } from '@noodl-core-ui/components/layout/Box';
|
||||
import { HStack } from '@noodl-core-ui/components/layout/Stack';
|
||||
import { UserBadge, UserBadgeSize } from '@noodl-core-ui/components/user/UserBadge';
|
||||
import { Slot } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './AiChatMessage.module.scss';
|
||||
|
||||
export type AiChatUser =
|
||||
| {
|
||||
role: 'user';
|
||||
name: string;
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
}
|
||||
| null;
|
||||
|
||||
export interface AiChatMessageProps {
|
||||
user?: AiChatUser;
|
||||
content: string;
|
||||
|
||||
affix?: Slot;
|
||||
}
|
||||
|
||||
export function AiChatMessage({ user, content, affix }: AiChatMessageProps) {
|
||||
return (
|
||||
<Box hasXSpacing hasYSpacing UNSAFE_className={css['Root']}>
|
||||
<HStack UNSAFE_style={{ height: 'auto', minHeight: '18px' }}>
|
||||
{Boolean(user) && (
|
||||
<Box hasRightSpacing>
|
||||
<div style={{ position: 'relative', width: '18px' }}>
|
||||
{user.role === 'user' && (
|
||||
<UserBadge size={UserBadgeSize.Tiny} name={user.name} email={user.name} id={user.name} />
|
||||
)}
|
||||
{user.role === 'assistant' && (
|
||||
<AiIcon
|
||||
UNSAFE_style={{
|
||||
position: 'absolute',
|
||||
left: '-3px',
|
||||
top: '-2px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
<Markdown content={content} UNSAFE_style={{ marginTop: '2px', userSelect: 'text' }} />
|
||||
</HStack>
|
||||
{Boolean(affix) && <Box hasTopSpacing>{affix}</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AiChatMessage';
|
||||
@@ -0,0 +1,23 @@
|
||||
.Root {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--theme-color-bg-3);
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
}
|
||||
|
||||
.Mounted {
|
||||
transform: translateY(0px);
|
||||
opacity: 1;
|
||||
transition: background-color 100ms ease, opacity 500ms ease, transform 0.5s ease;
|
||||
}
|
||||
|
||||
.Mounting {
|
||||
transform: translateY(8px);
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { PrimaryButton, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
|
||||
import { Box } from '@noodl-core-ui/components/layout/Box';
|
||||
|
||||
import { AiChatSuggestion } from './AiChatSuggestion';
|
||||
|
||||
export default {
|
||||
title: 'Ai/Ai Chat Suggestion',
|
||||
component: AiChatSuggestion,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof AiChatSuggestion>;
|
||||
|
||||
const Template: ComponentStory<typeof AiChatSuggestion> = (args) => (
|
||||
<div style={{ maxWidth: '280px' }}>
|
||||
<AiChatSuggestion {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {
|
||||
text: 'What are the required inputs for this node to work correctly?'
|
||||
};
|
||||
|
||||
export const IsLoading = Template.bind({});
|
||||
IsLoading.args = {
|
||||
isLoading: true
|
||||
};
|
||||
|
||||
export const OnUpdate = () => {
|
||||
const [count, setCount] = useState(1);
|
||||
return (
|
||||
<div style={{ maxWidth: '280px' }}>
|
||||
<AiChatSuggestion text={`Count: ${count}`} />
|
||||
<Box hasTopSpacing>
|
||||
<PrimaryButton
|
||||
label="Increment"
|
||||
variant={PrimaryButtonVariant.Muted}
|
||||
isGrowing
|
||||
onClick={() => setCount((prev) => prev + 1)}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { MouseEventHandler, useEffect, useState } from 'react';
|
||||
|
||||
import { ActivityIndicator } from '@noodl-core-ui/components/common/ActivityIndicator';
|
||||
import { Box } from '@noodl-core-ui/components/layout/Box';
|
||||
import { Center } from '@noodl-core-ui/components/layout/Center';
|
||||
import { Text } from '@noodl-core-ui/components/typography/Text';
|
||||
|
||||
import css from './AiChatSuggestion.module.scss';
|
||||
|
||||
export interface AiChatSuggestionProps {
|
||||
text?: string;
|
||||
isLoading?: boolean;
|
||||
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function AiChatSuggestion({ text, isLoading, onClick }: AiChatSuggestionProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box hasXSpacing hasYSpacing={1}>
|
||||
<div className={classNames([css['Root'], mounted ? css['Mounted'] : css['Mounting']])} onClick={onClick}>
|
||||
{isLoading ? (
|
||||
<Center>
|
||||
<ActivityIndicator />
|
||||
</Center>
|
||||
) : (
|
||||
<Text>“{text}”</Text>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AiChatSuggestion';
|
||||
@@ -0,0 +1,9 @@
|
||||
.Root {
|
||||
height: 100%;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AiChatboxError } from './AiChatboxError';
|
||||
|
||||
export default {
|
||||
title: 'Ai/Ai Chatbox Error',
|
||||
component: AiChatboxError,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof AiChatboxError>;
|
||||
|
||||
const Template: ComponentStory<typeof AiChatboxError> = (args) => (
|
||||
<div style={{ maxWidth: '380px', height: '800px', border: '1px solid black' }}>
|
||||
<AiChatboxError {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
|
||||
export const NotFound = Template.bind({});
|
||||
NotFound.args = {
|
||||
content:
|
||||
'Cannot find the chat history for this node. Could it be that the chat history is missing in Version Control? :('
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Label, LabelSize } from '@noodl-core-ui/components/typography/Label';
|
||||
import { Text } from '@noodl-core-ui/components/typography/Text';
|
||||
|
||||
import css from './AiChatboxError.module.scss';
|
||||
|
||||
export interface AiChatboxErrorProps {
|
||||
title?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function AiChatboxError({ title = 'Aw, Snap!', content }: AiChatboxErrorProps) {
|
||||
return (
|
||||
<div className={css['Root']}>
|
||||
<Label size={LabelSize.Big} hasBottomSpacing>
|
||||
{title}
|
||||
</Label>
|
||||
<Text style={{ textAlign: 'center' }}>{content}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AiChatboxError';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AiIcon } from './AiIcon';
|
||||
|
||||
export default {
|
||||
title: 'Ai/Ai Icon',
|
||||
component: AiIcon,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof AiIcon>;
|
||||
|
||||
const Template: ComponentStory<typeof AiIcon> = (args) => <AiIcon {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
100
packages/noodl-core-ui/src/components/ai/AiIcon/AiIcon.tsx
Normal file
100
packages/noodl-core-ui/src/components/ai/AiIcon/AiIcon.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
|
||||
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
|
||||
export interface AiIconProps extends UnsafeStyleProps {}
|
||||
|
||||
export function AiIcon({ UNSAFE_className, UNSAFE_style }: AiIconProps) {
|
||||
const gradientId = 'd2d05813-26d2-4c03-812e-75f68a94b149';
|
||||
const gradientUrl = `url(#${gradientId})`;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={UNSAFE_className}
|
||||
style={UNSAFE_style}
|
||||
>
|
||||
<path
|
||||
d="M10.9512 3.22576C11.5593 3.22576 12.0522 2.72751 12.0522 2.11288C12.0522 1.49825 11.5593 1 10.9512 1C10.3432 1 9.85022 1.49825 9.85022 2.11288C9.85022 2.72751 10.3432 3.22576 10.9512 3.22576Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M6.68969 4.40009C7.22049 4.40009 7.65079 3.96515 7.65079 3.42863C7.65079 2.8921 7.22049 2.45716 6.68969 2.45716C6.15889 2.45716 5.72859 2.8921 5.72859 3.42863C5.72859 3.96515 6.15889 4.40009 6.68969 4.40009Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M3.82576 6.73112C4.26115 6.73112 4.61411 6.37436 4.61411 5.93428C4.61411 5.49419 4.26115 5.13743 3.82576 5.13743C3.39037 5.13743 3.03742 5.49419 3.03742 5.93428C3.03742 6.37436 3.39037 6.73112 3.82576 6.73112Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M2.11012 9.34882C2.44573 9.34882 2.7178 9.07382 2.7178 8.73459C2.7178 8.39535 2.44573 8.12035 2.11012 8.12035C1.77451 8.12035 1.50244 8.39535 1.50244 8.73459C1.50244 9.07382 1.77451 9.34882 2.11012 9.34882Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M1.46048 12.4432C1.71479 12.4432 1.92095 12.2348 1.92095 11.9778C1.92095 11.7207 1.71479 11.5123 1.46048 11.5123C1.20616 11.5123 1 11.7207 1 11.9778C1 12.2348 1.20616 12.4432 1.46048 12.4432Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M1.78458 15.2657C1.96431 15.2657 2.11001 15.1185 2.11001 14.9368C2.11001 14.7551 1.96431 14.6079 1.78458 14.6079C1.60485 14.6079 1.45914 14.7551 1.45914 14.9368C1.45914 15.1185 1.60485 15.2657 1.78458 15.2657Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M13.0506 23C13.6587 23 14.1516 22.5017 14.1516 21.8871C14.1516 21.2725 13.6587 20.7742 13.0506 20.7742C12.4426 20.7742 11.9496 21.2725 11.9496 21.8871C11.9496 22.5017 12.4426 23 13.0506 23Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M17.3074 21.5417C17.8382 21.5417 18.2685 21.1067 18.2685 20.5702C18.2685 20.0337 17.8382 19.5988 17.3074 19.5988C16.7766 19.5988 16.3463 20.0337 16.3463 20.5702C16.3463 21.1067 16.7766 21.5417 17.3074 21.5417Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M20.1722 18.8626C20.6076 18.8626 20.9605 18.5058 20.9605 18.0657C20.9605 17.6256 20.6076 17.2689 20.1722 17.2689C19.7368 17.2689 19.3839 17.6256 19.3839 18.0657C19.3839 18.5058 19.7368 18.8626 20.1722 18.8626Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M21.8873 15.8798C22.2229 15.8798 22.4949 15.6048 22.4949 15.2656C22.4949 14.9264 22.2229 14.6514 21.8873 14.6514C21.5516 14.6514 21.2796 14.9264 21.2796 15.2656C21.2796 15.6048 21.5516 15.8798 21.8873 15.8798Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M22.5395 12.4879C22.7938 12.4879 23 12.2795 23 12.0224C23 11.7654 22.7938 11.557 22.5395 11.557C22.2852 11.557 22.079 11.7654 22.079 12.0224C22.079 12.2795 22.2852 12.4879 22.5395 12.4879Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M22.2142 9.39194C22.3939 9.39194 22.5396 9.24467 22.5396 9.06299C22.5396 8.88132 22.3939 8.73405 22.2142 8.73405C22.0344 8.73405 21.8887 8.88132 21.8887 9.06299C21.8887 9.24467 22.0344 9.39194 22.2142 9.39194Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M21.1878 6.79557C21.2847 6.79557 21.3633 6.71613 21.3633 6.61815C21.3633 6.52016 21.2847 6.44073 21.1878 6.44073C21.0908 6.44073 21.0123 6.52016 21.0123 6.61815C21.0123 6.71613 21.0908 6.79557 21.1878 6.79557Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M2.81427 17.6774C2.91122 17.6774 2.98981 17.5979 2.98981 17.5C2.98981 17.402 2.91122 17.3225 2.81427 17.3225C2.71732 17.3225 2.63873 17.402 2.63873 17.5C2.63873 17.5979 2.71732 17.6774 2.81427 17.6774Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M11.9629 14.689C11.9522 14.6574 11.9227 14.6361 11.8896 14.6361H8.30035C8.26724 14.6361 8.2378 14.6574 8.22707 14.689L7.6507 16.3907C7.63998 16.4223 7.61053 16.4436 7.57742 16.4436H5.78604C5.73253 16.4436 5.69514 16.3901 5.71306 16.3391L8.99863 6.99596C9.0096 6.96475 9.03883 6.94389 9.07161 6.94389H11.1317C11.1644 6.94389 11.1937 6.96475 11.2046 6.99596L14.4902 16.3391C14.5081 16.3901 14.4707 16.4436 14.4172 16.4436H12.6125C12.5794 16.4436 12.55 16.4223 12.5393 16.3907L11.9629 14.689ZM11.3308 13.114C11.384 13.114 11.4213 13.0611 11.4041 13.0103L10.1683 9.36187C10.1444 9.2913 10.0456 9.2913 10.0217 9.36187L8.78585 13.0103C8.76865 13.0611 8.806 13.114 8.85913 13.114H11.3308Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<path
|
||||
d="M17.5615 6.95748C17.6043 6.95748 17.6389 6.99253 17.6389 7.03576V16.3653C17.6389 16.4086 17.6043 16.4436 17.5615 16.4436H15.8531C15.8103 16.4436 15.7756 16.4086 15.7756 16.3653V7.03576C15.7756 6.99253 15.8103 6.95748 15.8531 6.95748H17.5615Z"
|
||||
fill={gradientUrl}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gradientId}
|
||||
x1="11.6737"
|
||||
y1="6.94385"
|
||||
x2="11.6737"
|
||||
y2="16.4436"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#B4B2FC" />
|
||||
<stop offset="1" stopColor="#A0EAEE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
1
packages/noodl-core-ui/src/components/ai/AiIcon/index.ts
Normal file
1
packages/noodl-core-ui/src/components/ai/AiIcon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './AiIcon'
|
||||
@@ -0,0 +1,259 @@
|
||||
.Root {
|
||||
// MIX BLEND MODE LIGTHEN MUST BE SET ON THE PARENT
|
||||
//mix-blend-mode: lighten;
|
||||
filter: brightness(150%);
|
||||
padding: 6px;
|
||||
transition: filter var(--speed-quick) var(--easing-base);
|
||||
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
mix-blend-mode: lighten;
|
||||
|
||||
&.__isDimmed {
|
||||
filter: brightness(110%);
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: var(--font-family);
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.HeroLogoWrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: inherit;
|
||||
opacity: 1;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
right: -3px;
|
||||
bottom: -3px;
|
||||
background: linear-gradient(0deg, var(--base-color-teal-400) 0%, var(--base-color-node-purple-600)) 45%;
|
||||
animation: circle 3s linear infinite;
|
||||
filter: saturate(120%) brightness(120%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes circle {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.HeroLogoWrapper > .LogoIdleCircle {
|
||||
box-shadow: 0 0 10px 10px white;
|
||||
}
|
||||
|
||||
.HeroLogo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: black;
|
||||
mix-blend-mode: multiply;
|
||||
|
||||
> span {
|
||||
color: #ffffff;
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
.LogoIdleCircleContainer {
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: scale(1);
|
||||
transition: transform var(--speed-quick) var(--easing-base);
|
||||
|
||||
.Root.__isListening & {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
.LogoIdleCircleInner {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
animation: shine 5s var(--easing-base) infinite;
|
||||
}
|
||||
|
||||
.LogoIdleCircle {
|
||||
position: absolute;
|
||||
border: 4px dotted white;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
animation: round 100s linear infinite reverse;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0%,
|
||||
50% {
|
||||
transform: scale(1);
|
||||
filter: blur(0px) brightness(100%);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(0.95);
|
||||
filter: blur(0.5px) brightness(200%) saturate(200%);
|
||||
}
|
||||
}
|
||||
|
||||
.LogoLoader {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: round 40s linear infinite;
|
||||
}
|
||||
|
||||
.LogoLoaderInner {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 7px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(0);
|
||||
transition: transform var(--speed-quick) var(--easing-base);
|
||||
|
||||
.Root.__isListening & {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.Circle1 {
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.Circle2 {
|
||||
position: absolute;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
|
||||
.SpinningBalls {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
text-indent: -9999em;
|
||||
overflow: hidden;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
transform: translateZ(0);
|
||||
animation: mltShdSpin 2.5s infinite ease, round 2.5s infinite ease;
|
||||
display: block;
|
||||
}
|
||||
|
||||
$_val1: calc(-0.83em * 1);
|
||||
$_ballSizeMod: 10;
|
||||
|
||||
@keyframes mltShdSpin {
|
||||
0% {
|
||||
box-shadow: 0 $_val1 0 calc(-0.4em * $_ballSizeMod), 0 $_val1 0 -0.42em, 0 $_val1 0 -0.44em, 0 $_val1 0 -0.46em,
|
||||
0 $_val1 0 -0.477em;
|
||||
}
|
||||
|
||||
5%,
|
||||
95% {
|
||||
box-shadow: 0 $_val1 0 calc(-0.4em), 0 $_val1 0 -0.42em, 0 $_val1 0 -0.44em, 0 $_val1 0 -0.46em, 0 $_val1 0 -0.477em;
|
||||
}
|
||||
|
||||
10%,
|
||||
59% {
|
||||
box-shadow: 0 $_val1 0 calc(-0.4em), -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em,
|
||||
-0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
|
||||
}
|
||||
|
||||
20% {
|
||||
box-shadow: 0 $_val1 0 calc(-0.4em), -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em,
|
||||
-0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
|
||||
}
|
||||
|
||||
38% {
|
||||
box-shadow: 0 $_val1 0 calc(-0.4em), -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em,
|
||||
-0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 $_val1 0 calc(-0.4em), 0 $_val1 0 -0.42em, 0 $_val1 0 -0.44em, 0 $_val1 0 -0.46em, 0 $_val1 0 -0.477em;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes round-half {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes round {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// @keyframes mltShdSpin {
|
||||
// 0% {
|
||||
// box-shadow: 0 $_val1 0 calc(-0.4em * $_ballSizeMod),
|
||||
// 0 $_val1 0 -0.42em, 0 $_val1 0 -0.44em,
|
||||
// 0 $_val1 0 -0.46em, 0 $_val1 0 -0.477em;
|
||||
// }
|
||||
|
||||
// 2.5%,
|
||||
// 47.5% {
|
||||
// box-shadow: 0 $_val1 0 calc(-0.4em),
|
||||
// 0 $_val1 0 -0.42em, 0 $_val1 0 -0.44em,
|
||||
// 0 $_val1 0 -0.46em, 0 $_val1 0 -0.477em;
|
||||
// }
|
||||
|
||||
// 5%,
|
||||
// 29.5% {
|
||||
// box-shadow: 0 $_val1 0 calc(-0.4em),
|
||||
// -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em,
|
||||
// -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
|
||||
// }
|
||||
|
||||
// 10% {
|
||||
// box-shadow: 0 $_val1 0 calc(-0.4em), -0.338em -0.758em 0 -0.42em,
|
||||
// -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em,
|
||||
// -0.749em -0.34em 0 -0.477em;
|
||||
// }
|
||||
|
||||
// 19% {
|
||||
// box-shadow: 0 $_val1 0 calc(-0.4em), -0.377em -0.74em 0 -0.42em,
|
||||
// -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em,
|
||||
// -0.82em -0.09em 0 -0.477em;
|
||||
// }
|
||||
|
||||
// 50%, 100% {
|
||||
// box-shadow: 0 $_val1 0 calc(-0.4em), 0 $_val1 0 -0.42em,
|
||||
// 0 $_val1 0 -0.44em, 0 $_val1 0 -0.46em, 0 $_val1 0 -0.477em;
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AiIconAnimated } from './AiIconAnimated';
|
||||
|
||||
export default {
|
||||
title: 'Ai/Ai Icon Animated',
|
||||
component: AiIconAnimated,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof AiIconAnimated>;
|
||||
|
||||
const Template: ComponentStory<typeof AiIconAnimated> = (args) => (
|
||||
<div
|
||||
style={{
|
||||
// A background is required for the mask to work
|
||||
backgroundColor: 'var(--theme-color-bg-3)'
|
||||
}}
|
||||
>
|
||||
<AiIconAnimated {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
|
||||
export const Listening = Template.bind({});
|
||||
Listening.args = {
|
||||
isListening: true
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './AiIconAnimated.module.scss';
|
||||
|
||||
export interface AiIconAnimatedProps extends UnsafeStyleProps {
|
||||
isListening?: boolean;
|
||||
isDimmed?: boolean;
|
||||
}
|
||||
|
||||
export function AiIconAnimated({ isListening, isDimmed, UNSAFE_className, UNSAFE_style }: AiIconAnimatedProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(css.Root, isListening && css.__isListening, isDimmed && css.__isDimmed, UNSAFE_className)}
|
||||
style={UNSAFE_style}
|
||||
>
|
||||
<div className={css.HeroLogoWrapper}>
|
||||
<div className={css.HeroLogo}>
|
||||
<span>Ai</span>
|
||||
|
||||
<div className={css.LogoIdleCircleContainer}>
|
||||
<div className={css.LogoIdleCircleInner}>
|
||||
<div className={css.LogoIdleCircle} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={css.LogoLoader}>
|
||||
<div className={css.LogoLoaderInner}>
|
||||
<div className={css.Circle1}>
|
||||
<span className={css.SpinningBalls}></span>
|
||||
</div>
|
||||
|
||||
<div className={css.Circle2}>
|
||||
<span className={css.SpinningBalls}></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AiIconAnimated';
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
export interface ISideNavigationContext {
|
||||
isShowingTooltips: boolean;
|
||||
setIsShowingTooltips: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const SideNavigationContext = createContext<ISideNavigationContext>({
|
||||
isShowingTooltips: null,
|
||||
setIsShowingTooltips: null
|
||||
});
|
||||
|
||||
export function SideNavigationContextProvider({ children }) {
|
||||
const [isShowingTooltips, setIsShowingTooltips] = useState(false);
|
||||
|
||||
return (
|
||||
<SideNavigationContext.Provider
|
||||
value={{
|
||||
isShowingTooltips,
|
||||
setIsShowingTooltips
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SideNavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSideNavigationContext() {
|
||||
const context = useContext(SideNavigationContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useSideNavigationContext must be a child of SideNavigationContextProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
$_sidebar-hover-enter-offset: 250ms;
|
||||
|
||||
.Root {
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--theme-color-bg-1);
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.Toolbar {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 40px;
|
||||
height: 100%;
|
||||
border-right: 2px solid var(--theme-color-bg-1);
|
||||
background-color: var(--theme-color-bg-2);
|
||||
transition: box-shadow var(--speed-quick) var(--easing-base);
|
||||
|
||||
&:has(.Label.is-tooltip-visible) {
|
||||
transition: box-shadow var(--speed-quick) var(--easing-base) $_sidebar-hover-enter-offset;
|
||||
box-shadow: 0 0 30px 5px var(--theme-color-bg-1-transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.Logo {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.ToolbarIcon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
position: relative;
|
||||
left: 8px;
|
||||
margin-bottom: 20px;
|
||||
vertical-align: middle;
|
||||
filter: grayscale(100%) brightness(150%);
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.Panel {
|
||||
flex: 1;
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
margin-left: 40px;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--theme-color-bg-2);
|
||||
border-left: 2px solid var(--theme-color-bg-1);
|
||||
}
|
||||
|
||||
.SideNavigationButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
path {
|
||||
fill: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Label {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
transition: max-width var(--speed-quick) var(--easing-base);
|
||||
cursor: pointer;
|
||||
|
||||
&.is-tooltip-visible {
|
||||
transition: max-width var(--speed-quick) var(--easing-base) $_sidebar-hover-enter-offset;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.LabelInner {
|
||||
padding-left: 8px;
|
||||
padding-right: 16px;
|
||||
|
||||
.SideNavigationButton:hover &:not(.is-active) {
|
||||
p {
|
||||
color: var(--theme-color-fg-default);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.Command {
|
||||
height: 0;
|
||||
transition: height var(--speed-quick) var(--easing-base);
|
||||
|
||||
.SideNavigationButton:hover & {
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.IconButtonContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.NotificationBadge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 2px 5px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--theme-color-secondary-bright);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-family);
|
||||
color: var(--theme-color-on-secondary);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { IconName } from '@noodl-core-ui/components/common/Icon';
|
||||
import { PrimaryButton } from '@noodl-core-ui/components/inputs/PrimaryButton';
|
||||
import { Container, ContainerDirection } from '@noodl-core-ui/components/layout/Container';
|
||||
|
||||
import { SideNavigation, SideNavigationButton } from './SideNavigation';
|
||||
|
||||
export default {
|
||||
title: 'App/Side Navigation',
|
||||
component: SideNavigation,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof SideNavigation>;
|
||||
|
||||
const Template: ComponentStory<typeof SideNavigation> = (args) => (
|
||||
<div style={{ width: '380px', height: '800px' }}>
|
||||
<SideNavigation {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {
|
||||
toolbar: (
|
||||
<>
|
||||
<Container direction={ContainerDirection.Vertical} UNSAFE_style={{ flex: '1' }}>
|
||||
<SideNavigationButton icon={IconName.Components} label={'Components'} />
|
||||
<SideNavigationButton icon={IconName.Search} label={'Search'} />
|
||||
<SideNavigationButton icon={IconName.Collaboration} label={'Collaboration'} />
|
||||
<SideNavigationButton icon={IconName.StructureCircle} label={'Version control'} notification={{ count: 2 }} />
|
||||
<SideNavigationButton icon={IconName.StructureCircle} label={'Version control'} notification={{ count: 100 }} />
|
||||
<SideNavigationButton icon={IconName.CloudData} label={'Cloud Services'} />
|
||||
<SideNavigationButton icon={IconName.CloudFunction} label={'Cloud functions'} />
|
||||
<SideNavigationButton icon={IconName.Setting} label={'Project settings'} />
|
||||
</Container>
|
||||
<Container direction={ContainerDirection.Vertical}>
|
||||
<SideNavigationButton icon={IconName.SlidersHorizontal} label={'Editor settings'} />
|
||||
</Container>
|
||||
</>
|
||||
),
|
||||
panel: (
|
||||
<Container hasXSpacing hasYSpacing>
|
||||
<PrimaryButton label="Hello World" isGrowing />
|
||||
</Container>
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
SideNavigationContextProvider,
|
||||
useSideNavigationContext
|
||||
} from '@noodl-core-ui/components/app/SideNavigation/SideNavigation.context';
|
||||
import { IconName } from '@noodl-core-ui/components/common/Icon';
|
||||
import { IconButton, IconButtonState, IconButtonVariant } from '@noodl-core-ui/components/inputs/IconButton';
|
||||
import { DialogRenderDirection } from '@noodl-core-ui/components/layout/BaseDialog';
|
||||
import { MenuDialog, MenuDialogProps } from '@noodl-core-ui/components/popups/MenuDialog';
|
||||
import { Tooltip } from '@noodl-core-ui/components/popups/Tooltip';
|
||||
import { Label, LabelSize } from '@noodl-core-ui/components/typography/Label';
|
||||
import { Text, TextType } from '@noodl-core-ui/components/typography/Text';
|
||||
import { Slot } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './SideNavigation.module.scss';
|
||||
|
||||
export interface SideNavigationButtonProps {
|
||||
isActive?: boolean;
|
||||
icon: IconName;
|
||||
label: string;
|
||||
fineType?: string;
|
||||
notification?: { count: number };
|
||||
isDisabled?: boolean;
|
||||
testId?: string;
|
||||
onClick?: () => void;
|
||||
menuItems?: MenuDialogProps['items'];
|
||||
}
|
||||
|
||||
export function SideNavigationButton({
|
||||
isActive,
|
||||
icon,
|
||||
label,
|
||||
fineType,
|
||||
notification,
|
||||
isDisabled,
|
||||
testId,
|
||||
onClick,
|
||||
menuItems
|
||||
}: SideNavigationButtonProps) {
|
||||
const context = useSideNavigationContext();
|
||||
const iconRef = useRef();
|
||||
const hasMenu = Boolean(menuItems);
|
||||
const [isMenuVisible, setIsMenuVisible] = useState(false);
|
||||
|
||||
// NOTE: Commented out extending sidebar labels in case we want to bring them back at some point
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css['SideNavigationButton']}
|
||||
onClick={() => {
|
||||
!isDisabled && onClick && onClick();
|
||||
//context.setIsShowingTooltips(false);
|
||||
}}
|
||||
// onMouseEnter={() => context.setIsShowingTooltips(true)}
|
||||
// onMouseLeave={() => context.setIsShowingTooltips(false)}
|
||||
data-test={testId}
|
||||
>
|
||||
{hasMenu && (
|
||||
<MenuDialog
|
||||
items={menuItems}
|
||||
onClose={() => setIsMenuVisible(false)}
|
||||
triggerRef={iconRef}
|
||||
isVisible={isMenuVisible}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={css['IconButtonContainer']} ref={iconRef} onClick={() => hasMenu && setIsMenuVisible(true)}>
|
||||
<Tooltip
|
||||
content={label}
|
||||
fineType={fineType}
|
||||
renderDirection={DialogRenderDirection.Horizontal}
|
||||
showAfterMs={300}
|
||||
>
|
||||
<IconButton
|
||||
variant={IconButtonVariant.Transparent}
|
||||
state={isActive ? IconButtonState.Active : IconButtonState.Default}
|
||||
icon={icon}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
{notification && (
|
||||
<div className={css['NotificationBadge']}>{notification.count > 99 ? '99+' : notification.count}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <div
|
||||
className={classNames(css['Label'], context.isShowingTooltips && css['is-tooltip-visible'])}
|
||||
onClick={() => hasMenu && setIsMenuVisible(true)}
|
||||
>
|
||||
<div className={classNames(css['LabelInner'], isActive && css['is-active'])}>
|
||||
<Text textType={isActive ? TextType.Proud : TextType.Shy}>{label}</Text>
|
||||
{fineType && (
|
||||
<Label size={LabelSize.Small} variant={TextType.Shy} UNSAFE_className={css['Command']}>
|
||||
{fineType}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SideNavigationProps {
|
||||
toolbar: Slot;
|
||||
panel: Slot;
|
||||
|
||||
onExitClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function SideNavigation({ toolbar, panel, onExitClick }: SideNavigationProps) {
|
||||
return (
|
||||
<SideNavigationContextProvider>
|
||||
<div className={css['Root']}>
|
||||
<div className={css['Panel']}>{panel}</div>
|
||||
|
||||
<div className={css['Toolbar']}>
|
||||
<div className={css['Logo']}>
|
||||
<SideNavigationButton
|
||||
icon={IconName.Logo}
|
||||
label="Exit project"
|
||||
menuItems={[{ label: 'Exit project', isDangerous: true, onClick: onExitClick }]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{toolbar}
|
||||
</div>
|
||||
</div>
|
||||
</SideNavigationContextProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SideNavigation';
|
||||
@@ -0,0 +1,107 @@
|
||||
.Root {
|
||||
height: 30px;
|
||||
flex-basis: 30px;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
width: 100%;
|
||||
-webkit-app-region: drag;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid var(--theme-color-bg-1);
|
||||
background-color: var(--theme-color-bg-2);
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
|
||||
&.is-variant-shallow {
|
||||
background-color: var(--theme-color-bg-1);
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.Title {
|
||||
font-family: var(--font-family);
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.Version {
|
||||
font-family: var(--font-family);
|
||||
color: #c4c4c4;
|
||||
font-size: 11px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.OSWindows {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.OSWindowsIcon {
|
||||
-webkit-app-region: no-drag;
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.OSWindowsIcon__Minimize:before {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--theme-color-bg-1);
|
||||
border-color: var(--theme-color-fg-highlight);
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.OSWindowsIcon__Maximize:before {
|
||||
content: '';
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px solid var(--theme-color-fg-highlight);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.OSWindowsIcon__Close {
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 14px;
|
||||
width: 1px;
|
||||
background-color: var(--theme-color-fg-highlight);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-color-danger);
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TitleBar, TitleBarState } from './TitleBar';
|
||||
|
||||
export default {
|
||||
title: 'App/Title Bar',
|
||||
component: TitleBar,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof TitleBar>;
|
||||
|
||||
const Template: ComponentStory<typeof TitleBar> = (args) => (
|
||||
<div style={{ position: 'relative', width: 950, height: 40 }}>
|
||||
<TitleBar {...args}></TitleBar>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {
|
||||
title: 'Noodl Storybook',
|
||||
version: '2.6.5',
|
||||
isWindows: false
|
||||
};
|
||||
|
||||
export const IsWindows = Template.bind({});
|
||||
IsWindows.args = {
|
||||
title: 'Noodl Storybook',
|
||||
version: '2.6.5',
|
||||
isWindows: true
|
||||
};
|
||||
|
||||
export const UpdateAvailable = Template.bind({});
|
||||
UpdateAvailable.args = {
|
||||
title: 'Noodl Storybook',
|
||||
version: '2.6.5',
|
||||
versionAvailable: '2.6.6',
|
||||
state: TitleBarState.UpdateAvailable,
|
||||
isWindows: true
|
||||
};
|
||||
|
||||
export const Updated = Template.bind({});
|
||||
Updated.args = {
|
||||
title: 'Noodl Storybook',
|
||||
version: '2.6.5',
|
||||
state: TitleBarState.Updated,
|
||||
isWindows: true
|
||||
};
|
||||
107
packages/noodl-core-ui/src/components/app/TitleBar/TitleBar.tsx
Normal file
107
packages/noodl-core-ui/src/components/app/TitleBar/TitleBar.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import { FeedbackType } from '@noodl-constants/FeedbackType';
|
||||
|
||||
import { TextButton, TextButtonSize } from '@noodl-core-ui/components/inputs/TextButton';
|
||||
|
||||
import css from './TitleBar.module.scss';
|
||||
|
||||
export enum TitleBarVariant {
|
||||
Default = 'default',
|
||||
Shallow = 'shallow'
|
||||
}
|
||||
|
||||
export enum TitleBarState {
|
||||
Default = 'default',
|
||||
UpdateAvailable = 'version-available',
|
||||
Updated = 'version-updated'
|
||||
}
|
||||
|
||||
export interface TitleBarProps {
|
||||
title: string;
|
||||
version?: string;
|
||||
|
||||
state?: TitleBarState;
|
||||
variant?: TitleBarVariant;
|
||||
|
||||
onNewVersionAvailableClicked?: () => void;
|
||||
onNewUpdateAvailableClicked?: () => void;
|
||||
|
||||
isWindows?: boolean;
|
||||
onMinimizeClicked?: () => void;
|
||||
onMaximizeClicked?: () => void;
|
||||
onCloseClicked?: () => void;
|
||||
}
|
||||
|
||||
export function TitleBar({
|
||||
title,
|
||||
version,
|
||||
state = TitleBarState.Default,
|
||||
variant = TitleBarVariant.Default,
|
||||
onNewVersionAvailableClicked,
|
||||
onNewUpdateAvailableClicked,
|
||||
isWindows,
|
||||
onMinimizeClicked,
|
||||
onMaximizeClicked,
|
||||
onCloseClicked
|
||||
}: TitleBarProps) {
|
||||
return (
|
||||
<div className={classNames([css['Root'], css[`is-variant-${variant}`]])}>
|
||||
<div className={classNames([css['Title']])}>{title}</div>
|
||||
|
||||
{Boolean(variant === TitleBarVariant.Default) && (
|
||||
<>
|
||||
{state === TitleBarState.UpdateAvailable && (
|
||||
<TextButton
|
||||
label={`New version available`}
|
||||
onClick={onNewVersionAvailableClicked}
|
||||
size={TextButtonSize.Small}
|
||||
variant={FeedbackType.Notice}
|
||||
//@ts-ignore
|
||||
UNSAFE_style={{ WebkitAppRegion: 'no-drag' }} //make it clickable
|
||||
hasLeftSpacing
|
||||
hasRightSpacing
|
||||
/>
|
||||
)}
|
||||
|
||||
{state === TitleBarState.Updated && (
|
||||
<TextButton
|
||||
label="New update downloaded"
|
||||
onClick={onNewUpdateAvailableClicked}
|
||||
size={TextButtonSize.Small}
|
||||
variant={FeedbackType.Notice}
|
||||
//@ts-ignore
|
||||
UNSAFE_style={{ WebkitAppRegion: 'no-drag' }} //make it clickable
|
||||
hasLeftSpacing
|
||||
hasRightSpacing
|
||||
/>
|
||||
)}
|
||||
|
||||
{Boolean(version) && <div className={classNames(css['Version'])}>{version}</div>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{Boolean(isWindows) && (
|
||||
<div className={classNames(css['OSWindows'])}>
|
||||
{Boolean(variant === TitleBarVariant.Default) && (
|
||||
<>
|
||||
<div
|
||||
className={classNames([css['OSWindowsIcon'], css['OSWindowsIcon__Minimize']])}
|
||||
onClick={onMinimizeClicked}
|
||||
></div>
|
||||
<div
|
||||
className={classNames([css['OSWindowsIcon'], css['OSWindowsIcon__Maximize']])}
|
||||
onClick={onMaximizeClicked}
|
||||
></div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={classNames([css['OSWindowsIcon'], css['OSWindowsIcon__Close']])}
|
||||
onClick={onCloseClicked}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './TitleBar';
|
||||
@@ -0,0 +1,60 @@
|
||||
.Root {
|
||||
&.is-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--theme-color-bg-1-transparent);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.Inner {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
|
||||
&.is-size-small {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.FirstDot,
|
||||
.SecondDot,
|
||||
.ThirdDot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 3px;
|
||||
|
||||
border-radius: 100%;
|
||||
animation: bouncedelay 1.4s infinite var(--easing-base) both;
|
||||
|
||||
&.is-color-light {
|
||||
background-color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
&.is-color-dark {
|
||||
background-color: var(--theme-color-bg-1);
|
||||
}
|
||||
}
|
||||
|
||||
.FirstDot {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.SecondDot {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@keyframes bouncedelay {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { ActivityIndicator } from './ActivityIndicator';
|
||||
|
||||
export default {
|
||||
title: 'Common/Activity Indicator',
|
||||
component: ActivityIndicator,
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof ActivityIndicator>;
|
||||
|
||||
const Template: ComponentStory<typeof ActivityIndicator> = (args) => (
|
||||
<ActivityIndicator {...args} />
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,39 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import css from './ActivityIndicator.module.scss';
|
||||
|
||||
export enum ActivityIndicatorColor {
|
||||
Light = 'light',
|
||||
Dark = 'dark'
|
||||
}
|
||||
|
||||
export enum ActivityIndicatorSize {
|
||||
Default = 'default',
|
||||
Small = 'small'
|
||||
}
|
||||
|
||||
export interface ActivityIndicatorProps {
|
||||
text?: string;
|
||||
color?: ActivityIndicatorColor;
|
||||
size?: ActivityIndicatorSize;
|
||||
isOverlay?: boolean;
|
||||
}
|
||||
|
||||
export function ActivityIndicator({
|
||||
text,
|
||||
color = ActivityIndicatorColor.Light,
|
||||
size = ActivityIndicatorSize.Default,
|
||||
isOverlay
|
||||
}: ActivityIndicatorProps) {
|
||||
return (
|
||||
<div className={classNames([css['Root'], isOverlay && css['is-overlay']])}>
|
||||
<div className={classNames(css['Inner'], css[`is-size-${size}`])}>
|
||||
{Boolean(text) && <p>{text}</p>}
|
||||
<div className={classNames([css['FirstDot'], css[`is-color-${color}`]])}></div>
|
||||
<div className={classNames([css['SecondDot'], css[`is-color-${color}`]])}></div>
|
||||
<div className={classNames([css['ThirdDot'], css[`is-color-${color}`]])}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ActivityIndicator';
|
||||
@@ -0,0 +1,43 @@
|
||||
.Root {
|
||||
border-radius: 2px;
|
||||
|
||||
&.is-bg {
|
||||
&-1,
|
||||
&-1-on-hover:hover {
|
||||
background-color: var(--theme-color-bg-1);
|
||||
}
|
||||
|
||||
&-2,
|
||||
&-2-on-hover:hover {
|
||||
background-color: var(--theme-color-bg-2);
|
||||
}
|
||||
|
||||
&-3,
|
||||
&-3-on-hover:hover {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
|
||||
&-4,
|
||||
&-4-on-hover:hover {
|
||||
background-color: var(--theme-color-bg-4);
|
||||
}
|
||||
}
|
||||
|
||||
&.has-hover-state {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.is-padding {
|
||||
&-small {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&-default {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&-large {
|
||||
padding: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Card } from './Card';
|
||||
|
||||
export default {
|
||||
title: 'Common/Card',
|
||||
component: Card,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof Card>;
|
||||
|
||||
const Template: ComponentStory<typeof Card> = (args) => <Card {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
57
packages/noodl-core-ui/src/components/common/Card/Card.tsx
Normal file
57
packages/noodl-core-ui/src/components/common/Card/Card.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { MouseEventHandler } from 'react';
|
||||
|
||||
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './Card.module.scss';
|
||||
|
||||
export enum CardPadding {
|
||||
Small = 'is-padding-small',
|
||||
Default = 'is-padding-default',
|
||||
Large = 'is-padding-large'
|
||||
}
|
||||
|
||||
export enum CardBackground {
|
||||
Bg1 = 'is-bg-1',
|
||||
Bg2 = 'is-bg-2',
|
||||
Bg3 = 'is-bg-3',
|
||||
Bg4 = 'is-bg-4'
|
||||
}
|
||||
|
||||
export interface CardProps extends UnsafeStyleProps {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
padding?: CardPadding;
|
||||
background?: CardBackground;
|
||||
hoverBackground?: CardBackground;
|
||||
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function Card({
|
||||
children,
|
||||
padding = CardPadding.Default,
|
||||
background = CardBackground.Bg3,
|
||||
hoverBackground,
|
||||
|
||||
onClick,
|
||||
|
||||
UNSAFE_className,
|
||||
UNSAFE_style
|
||||
}: CardProps) {
|
||||
return (
|
||||
<div
|
||||
style={UNSAFE_style}
|
||||
className={classNames(
|
||||
css.Root,
|
||||
css[padding],
|
||||
css[background],
|
||||
css[`${hoverBackground}-on-hover`],
|
||||
(Boolean(hoverBackground) || Boolean(onClick)) && css['has-hover-state'],
|
||||
UNSAFE_className
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Card';
|
||||
@@ -0,0 +1,29 @@
|
||||
.Root {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
word-break: break-word;
|
||||
background-color: var(--baseColor);
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
min-height: 36px;
|
||||
|
||||
&.is-highlighted {
|
||||
background-color: var(--highlightColor);
|
||||
}
|
||||
}
|
||||
|
||||
.Label {
|
||||
color: var(--textColor);
|
||||
display: block;
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
&.is-after-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { EditorNode } from './EditorNode';
|
||||
|
||||
export default {
|
||||
title: 'Common/EditorNode',
|
||||
component: EditorNode,
|
||||
argTypes: {
|
||||
item: {
|
||||
defaultValue: {
|
||||
name: 'Group',
|
||||
displayName: 'Group'
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
defaultValue: {
|
||||
base: '#315272',
|
||||
baseHighlighted: '#4d6784',
|
||||
header: '#173E5D',
|
||||
headerHighlighted: '#315272',
|
||||
outline: '#173E5D',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#cfd5de'
|
||||
}
|
||||
}
|
||||
}
|
||||
} as ComponentMeta<typeof EditorNode>;
|
||||
|
||||
const Template: ComponentStory<typeof EditorNode> = (args) => <EditorNode {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,71 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { CSSProperties } from 'react';
|
||||
|
||||
import { INodeType, INodeColorScheme } from '@noodl-types/nodeTypes';
|
||||
|
||||
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
|
||||
|
||||
import css from './EditorNode.module.scss';
|
||||
|
||||
export interface EditorNodeProps {
|
||||
item: INodeType;
|
||||
colors: INodeColorScheme;
|
||||
isHighlighted?: boolean;
|
||||
}
|
||||
|
||||
function nodeNameToIconName(itemName: INodeType['name']) {
|
||||
switch (itemName) {
|
||||
case 'Group':
|
||||
return IconName.Group;
|
||||
case 'Text':
|
||||
return IconName.TextInBox;
|
||||
case 'Image':
|
||||
return IconName.Image;
|
||||
case 'Video':
|
||||
return IconName.Video;
|
||||
case 'Circle':
|
||||
return IconName.CircleOpen;
|
||||
case 'net.noodl.visual.icon':
|
||||
return IconName.Icon;
|
||||
case 'net.noodl.controls.button':
|
||||
return IconName.Button;
|
||||
case 'net.noodl.controls.checkbox':
|
||||
return IconName.CheckboxFilled;
|
||||
case 'net.noodl.controls.options':
|
||||
return IconName.DropdownLines;
|
||||
case 'net.noodl.controls.radiobutton':
|
||||
return IconName.Radiobutton;
|
||||
case 'Radio Button Group':
|
||||
return IconName.RadiobuttonGroup;
|
||||
case 'net.noodl.controls.range':
|
||||
return IconName.SlidersFilled;
|
||||
case 'net.noodl.controls.textinput':
|
||||
return IconName.TextInput;
|
||||
case 'net.noodl.visual.columns':
|
||||
return IconName.Columns;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function EditorNode({ item, colors, isHighlighted }: EditorNodeProps) {
|
||||
const iconName = nodeNameToIconName(item.name);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames([css['Root'], isHighlighted && css['is-highlighted']])}
|
||||
style={
|
||||
{
|
||||
'--textColor': colors.text,
|
||||
'--baseColor': colors.headerHighlighted,
|
||||
'--highlightColor': colors.baseHighlighted
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{iconName && <Icon icon={iconName} size={IconSize.Small} />}
|
||||
<span className={classNames(css['Label'], iconName && css['is-after-icon'])}>
|
||||
{item.displayName || item.displayNodeName || item.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './EditorNode'
|
||||
@@ -0,0 +1,35 @@
|
||||
.Root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.Center {
|
||||
height: 60%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Container {
|
||||
padding: 8px 16px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
box-sizing: border-box;
|
||||
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
padding: 16px 0;
|
||||
margin: 16px 0;
|
||||
background: var(--theme-color-bg-3);
|
||||
color: var(--theme-color-fg-default);
|
||||
border-radius: 4px;
|
||||
user-select: text;
|
||||
|
||||
.Error {
|
||||
display: block;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { Text } from '@noodl-core-ui/components/typography/Text';
|
||||
|
||||
export default {
|
||||
title: 'Common/Error Boundary',
|
||||
component: ErrorBoundary,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof ErrorBoundary>;
|
||||
|
||||
export const Common = (args) => (
|
||||
<ErrorBoundary {...args}>
|
||||
<Text>Everything working fine</Text>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
function CauseError(): JSX.Element {
|
||||
let invalid_object = {};
|
||||
|
||||
// @ts-ignore
|
||||
invalid_object.value.toThrowError();
|
||||
|
||||
return <Text>Everything working fine</Text>;
|
||||
}
|
||||
|
||||
export const OnError = (args) => (
|
||||
<ErrorBoundary {...args}>
|
||||
<CauseError />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PrimaryButton, PrimaryButtonSize, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
|
||||
import { Box } from '@noodl-core-ui/components/layout/Box';
|
||||
import { Collapsible } from '@noodl-core-ui/components/layout/Collapsible';
|
||||
import { HStack } from '@noodl-core-ui/components/layout/Stack';
|
||||
import { Label, LabelSize } from '@noodl-core-ui/components/typography/Label';
|
||||
import { Text } from '@noodl-core-ui/components/typography/Text';
|
||||
import { Slot } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './ErrorBoundary.module.scss';
|
||||
|
||||
export interface ErrorBoundaryProps {
|
||||
showTryAgain?: boolean;
|
||||
onTryAgain?: () => void;
|
||||
|
||||
hideErrorStack?: boolean;
|
||||
hideCopyError?: boolean;
|
||||
|
||||
children: Slot;
|
||||
}
|
||||
|
||||
// https://reactjs.org/docs/error-boundaries.html
|
||||
export class ErrorBoundary extends React.Component<
|
||||
ErrorBoundaryProps,
|
||||
{
|
||||
error: any;
|
||||
errorInfo: any;
|
||||
showMore: boolean;
|
||||
}
|
||||
> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
showMore: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: any, errorInfo: any) {
|
||||
// Catch errors in any components below and re-render with error message
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
});
|
||||
// You can also log error messages to an error reporting service here
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errorInfo) {
|
||||
const onCopyError = () => {
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
// TODO: Add Noodl version etc
|
||||
error: this.state.error?.toString(),
|
||||
stack: this.state.errorInfo.componentStack
|
||||
})
|
||||
);
|
||||
// TODO: Add some user feedback, this component should probably be in the editor
|
||||
};
|
||||
|
||||
// Error path
|
||||
return (
|
||||
<div className={css['Root']}>
|
||||
<div className={css['Center']}>
|
||||
<Box hasXSpacing UNSAFE_style={{ width: '100%', boxSizing: 'border-box' }}>
|
||||
<Label size={LabelSize.Big} hasBottomSpacing>
|
||||
Aw, Snap!
|
||||
</Label>
|
||||
<Text>Something happened.</Text>
|
||||
{this.props.showTryAgain && (
|
||||
<Box hasTopSpacing>
|
||||
<HStack hasSpacing>
|
||||
<Box>
|
||||
<PrimaryButton
|
||||
size={PrimaryButtonSize.Small}
|
||||
label="Click here to try again"
|
||||
onClick={this.props.onTryAgain}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
{this.props.hideCopyError !== true && (
|
||||
<div style={{ position: 'absolute', bottom: 0, width: '100%' }}>
|
||||
<div style={{ background: 'var(--theme-color-bg-2)', borderTop: '1px solid var(--theme-color-bg-1)' }}>
|
||||
<div className={css['Container']}>
|
||||
<Collapsible isCollapsed={!this.state.showMore}>
|
||||
<pre>
|
||||
<span className={css['Error']}>{this.state.error && this.state.error.toString()}</span>
|
||||
<span>{this.state.errorInfo.componentStack}</span>
|
||||
</pre>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={css['Container']}>
|
||||
<HStack hasSpacing>
|
||||
{this.props.hideErrorStack !== true && (
|
||||
<Box>
|
||||
<PrimaryButton
|
||||
variant={PrimaryButtonVariant.Muted}
|
||||
size={PrimaryButtonSize.Small}
|
||||
label="More info"
|
||||
onClick={() =>
|
||||
this.setState((prev) => ({
|
||||
showMore: !prev.showMore
|
||||
}))
|
||||
}
|
||||
hasBottomSpacing
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<PrimaryButton
|
||||
variant={PrimaryButtonVariant.Ghost}
|
||||
size={PrimaryButtonSize.Small}
|
||||
label="Copy Error Message"
|
||||
onClick={onCopyError}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Normally, just render children
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ErrorBoundary';
|
||||
@@ -0,0 +1,103 @@
|
||||
.Root {
|
||||
:global {
|
||||
* {
|
||||
color: var(--theme-color-fg-default);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
padding-top: 1em;
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
strong,
|
||||
.ndl-node {
|
||||
color: var(--theme-color-fg-highlight);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
a {
|
||||
//color: #d49517;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-decoration: underline;
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
//color: #fdb314;
|
||||
text-decoration: none;
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
.language-javascript {
|
||||
color: #eee;
|
||||
background-color: #444;
|
||||
display: block;
|
||||
padding: 5px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.ndl-images {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ndl-image {
|
||||
height: auto;
|
||||
margin: 5px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.ndl-image.small {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.ndl-image.med {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.ndl-image.large {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ndl-data {
|
||||
color: var(--theme-color-data);
|
||||
}
|
||||
|
||||
.ndl-signal {
|
||||
color: var(--theme-color-signal);
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--theme-color-bg-1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
padding: 0px 4px 2px;
|
||||
margin: 0px 2px;
|
||||
background-color: var(--theme-color-fg-muted);
|
||||
font-family: var(--font-family-code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { HtmlRenderer } from './HtmlRenderer';
|
||||
import { Text } from '@noodl-core-ui/components/typography/Text';
|
||||
|
||||
export default {
|
||||
title: 'Common/HtmlRenderer',
|
||||
component: HtmlRenderer,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof HtmlRenderer>;
|
||||
|
||||
const Template: ComponentStory<typeof HtmlRenderer> = (args) => (
|
||||
<>
|
||||
<Text>Pass an HTML string to the html-prop</Text>
|
||||
<HtmlRenderer {...args} />;
|
||||
</>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import css from './HtmlRenderer.module.scss';
|
||||
|
||||
export interface HtmlRendererProps {
|
||||
html: string;
|
||||
}
|
||||
|
||||
export function HtmlRenderer({ html }: HtmlRendererProps) {
|
||||
return <div className={css['Root']} dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './HtmlRenderer'
|
||||
@@ -0,0 +1,66 @@
|
||||
.Root {
|
||||
display: block;
|
||||
|
||||
path {
|
||||
transition: fill var(--speed-turbo) var(--easing-base);
|
||||
}
|
||||
|
||||
&.is-size-default {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.is-size-large {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
&.is-size-small {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.is-size-tiny {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.is-variant {
|
||||
&-default path {
|
||||
fill: var(--theme-color-fg-default);
|
||||
}
|
||||
|
||||
&-default-contrast path {
|
||||
fill: var(--theme-color-fg-default-contrast) !important;
|
||||
}
|
||||
|
||||
&-proud path {
|
||||
fill: var(--theme-color-fg-highlight) !important;
|
||||
}
|
||||
|
||||
&-shy path {
|
||||
fill: var(--theme-color-fg-muted) !important;
|
||||
}
|
||||
|
||||
&-success path {
|
||||
fill: var(--theme-color-success) !important;
|
||||
}
|
||||
|
||||
&-notice path {
|
||||
fill: var(--theme-color-notice) !important;
|
||||
}
|
||||
|
||||
&-danger path {
|
||||
fill: var(--theme-color-danger) !important;
|
||||
}
|
||||
|
||||
&-secondary path {
|
||||
fill: var(--theme-color-secondary-as-fg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Icon, IconName } from './Icon';
|
||||
|
||||
export default {
|
||||
title: 'Common/Icon',
|
||||
component: Icon,
|
||||
argTypes: {
|
||||
icon: { control: 'select', options: IconName }
|
||||
}
|
||||
} as ComponentMeta<typeof Icon>;
|
||||
|
||||
const Template: ComponentStory<typeof Icon> = (args) => <Icon {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
224
packages/noodl-core-ui/src/components/common/Icon/Icon.tsx
Normal file
224
packages/noodl-core-ui/src/components/common/Icon/Icon.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { FeedbackType } from '@noodl-constants/FeedbackType';
|
||||
|
||||
import { TextType } from '@noodl-core-ui/components/typography/Text';
|
||||
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './Icon.module.scss';
|
||||
|
||||
export enum IconName {
|
||||
AlignItemCenter = 'align_item_center',
|
||||
AlignItemLeft = 'align_item_left',
|
||||
AlignItemRight = 'align_item_right',
|
||||
ArrowDown = 'arrow_down',
|
||||
ArrowLeft = 'arrow_left',
|
||||
ArrowLineDown = 'arrow_line_down',
|
||||
ArrowLineLeft = 'arrow_line_left',
|
||||
ArrowLineRight = 'arrow_line_right',
|
||||
ArrowLineUp = 'arrow_line_up',
|
||||
ArrowRight = 'arrow_right',
|
||||
ArrowUp = 'arrow_up',
|
||||
ArrowsInLineHorizontal = 'arrows_in_line_horizontal',
|
||||
ArrowsInLineVertical = 'arrows_in_line_vertical',
|
||||
BorderAll = 'border_all',
|
||||
BorderDown = 'border_down',
|
||||
BorderLeft = 'border_left',
|
||||
BorderRight = 'border_right',
|
||||
BorderUp = 'border_up',
|
||||
Bug = 'bug',
|
||||
Cards = 'cards',
|
||||
CaretUp = 'caret_up',
|
||||
CaretDown = 'caret_down',
|
||||
CaretDownUp = 'caret_down_up',
|
||||
CaretLeft = 'caret_left',
|
||||
CaretRight = 'caret_right',
|
||||
Chat = 'chat',
|
||||
ChatFill = 'chat_fill',
|
||||
Check = 'check',
|
||||
Close = 'close',
|
||||
CloudCheck = 'cloud_check',
|
||||
CloudData = 'cloud_data',
|
||||
CloudDownload = 'cloud_download',
|
||||
CloudUpload = 'cloud_upload',
|
||||
CloudFunction = 'cloud_function',
|
||||
Collaboration = 'collaboration',
|
||||
Code = 'code',
|
||||
Component = 'component',
|
||||
ComponentWithChildren = 'component_with_children',
|
||||
Components = 'components',
|
||||
ComponentsFill = 'components_fill',
|
||||
Copy = 'copy',
|
||||
Columns = 'columns',
|
||||
DeviceDesktop = 'device_desktop',
|
||||
DeviceLaptop = 'device_laptop',
|
||||
DevicePhone = 'device_phone',
|
||||
DeviceTablet = 'device_tablet',
|
||||
Dimension = 'dimension',
|
||||
DimensionHeight = 'dimension_height',
|
||||
DimensionWidthHeight = 'dimension_width_height',
|
||||
DimenstionWidth = 'dimenstion_width',
|
||||
DotsThree = 'dots_three',
|
||||
DotsThreeHorizontal = 'dots_three_horizontal',
|
||||
ExternalLink = 'external_link',
|
||||
File = 'file',
|
||||
FileFill = 'file_fill',
|
||||
FolderOpen = 'folder_open',
|
||||
FolderClosed = 'folder_closed',
|
||||
Home = 'home',
|
||||
HomeFill = 'home_fill',
|
||||
HorizontalSplit = 'horizontal_split',
|
||||
JustifyContentCenter = 'justify_content_center',
|
||||
JustifyContentEnd = 'justify_content_end',
|
||||
JustifyContentSpaceAround = 'justify_content_space_around',
|
||||
JustifyContentSpaceBetween = 'justify_content_space_between',
|
||||
JustifyContentSpaceEvenly = 'justify_content_space_evenly',
|
||||
JustifyContentStart = 'justify_content_start',
|
||||
Logo = 'logo',
|
||||
MagicWand = 'magic_wand',
|
||||
Minus = 'minus',
|
||||
NestedComponent = 'nested_component',
|
||||
NotePencil = 'note_pencil',
|
||||
Palette = 'palette',
|
||||
PaletteFill = 'palette_fill',
|
||||
PauseCircle = 'pause_circle',
|
||||
Pencil = 'pencil',
|
||||
PencilLine = 'pencil_line',
|
||||
Play = 'play',
|
||||
PlayCircle = 'play_circle',
|
||||
Plus = 'plus',
|
||||
PlusSquare = 'plus_square',
|
||||
PlusCircle = 'plus_circle',
|
||||
Question = 'question',
|
||||
QuestionFill = 'question_fill',
|
||||
QuestionFree = 'question_free',
|
||||
Refresh = 'refresh',
|
||||
Reset = 'reset',
|
||||
RestApi = 'rest_api',
|
||||
Rocket = 'rocket',
|
||||
Roll = 'roll',
|
||||
RoundedCornerAll = 'rounded_corner_all',
|
||||
RoundedCornerLeftDown = 'rounded_corner_left_down',
|
||||
RoundedCornerLeftUp = 'rounded_corner_left_up',
|
||||
RoundedCornerRightDown = 'rounded_corner_right_down',
|
||||
RoundedCornerRightUp = 'rounded_corner_right_up',
|
||||
Search = 'search',
|
||||
SearchCorner = 'search_corner',
|
||||
SearchGrid = 'search_grid',
|
||||
SearchSquare = 'search_square',
|
||||
Setting = 'setting',
|
||||
SettingFill = 'setting_fill',
|
||||
Sliders = 'sliders',
|
||||
SlidersHorizontal = 'sliders_horizontal',
|
||||
Stash = 'stash',
|
||||
StructureCircle = 'structure_circle',
|
||||
Square = 'square',
|
||||
SquareFilled = 'square_filled',
|
||||
SquareHalf = 'square_half',
|
||||
TextAlignCenter = 'text_align_center',
|
||||
TextAlignLeft = 'text_align_left',
|
||||
TextAlignRight = 'text_align_right',
|
||||
Trash = 'trash',
|
||||
User = 'user',
|
||||
UI = 'ui',
|
||||
VerticalSplit = 'vertical_split',
|
||||
ViewportDiagonalArrow = 'viewport_diagonal_arrow',
|
||||
ViewportHorizontalArrow = 'viewport_horizontal_arrow',
|
||||
ViewportVerticalArrow = 'viewport_vertical_arrow',
|
||||
WarningCircle = 'warning_circle',
|
||||
WarningCircleFilled = 'warning_circle_filled',
|
||||
WarningTriangle = 'warning_triangle',
|
||||
ImportDown = 'import_down',
|
||||
ImportLeft = 'import_left',
|
||||
ImportSlanted = 'import_slanted',
|
||||
Grip = 'grip',
|
||||
Group = 'group',
|
||||
TextInBox = 'text_in_box',
|
||||
Image = 'image',
|
||||
Video = 'video',
|
||||
CircleOpen = 'circle_open',
|
||||
CircleDot = 'circle_dot',
|
||||
Star = 'star',
|
||||
Button = 'button',
|
||||
Checkbox = 'checkbox',
|
||||
CheckboxFilled = 'checkbox_filled',
|
||||
Icon = 'icon',
|
||||
Dropdown = 'dropdown',
|
||||
DropdownLines = 'dropdown_lines',
|
||||
TextInput = 'text_input',
|
||||
PageRouter = 'page_router',
|
||||
PageInputArrow = 'page_input_arrow',
|
||||
Radiobutton = 'radiobutton',
|
||||
RadiobuttonGroupLine = 'radiobutton_group_line',
|
||||
RadiobuttonGroup = 'radiobutton_group',
|
||||
SlidersFilled = 'sliders_filled',
|
||||
Navigate = 'navigate',
|
||||
Link = 'link',
|
||||
SEO = 'seo'
|
||||
}
|
||||
|
||||
export enum IconSize {
|
||||
Default = 'is-size-default',
|
||||
Large = 'is-size-large',
|
||||
Small = 'is-size-small',
|
||||
Tiny = 'is-size-tiny'
|
||||
}
|
||||
|
||||
export type IconVariant = FeedbackType | TextType;
|
||||
|
||||
export interface IconProps extends UnsafeStyleProps {
|
||||
icon: IconName;
|
||||
size?: IconSize;
|
||||
variant?: IconVariant;
|
||||
}
|
||||
|
||||
/** Require all SVGs in folder so that we dont have to type them in manually */
|
||||
const reqSvgs = require.context('@noodl-core-ui/assets/icons/icon-component', true, /\.svg$/);
|
||||
|
||||
/** Create object from all required SVGs so that we can access them */
|
||||
const _IconObject = reqSvgs.keys().reduce((images, path) => {
|
||||
const key = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.'));
|
||||
images[key] = reqSvgs(path);
|
||||
return images;
|
||||
}, {});
|
||||
|
||||
export function Icon({
|
||||
icon = IconName.BorderAll,
|
||||
size = IconSize.Default,
|
||||
variant,
|
||||
UNSAFE_className,
|
||||
UNSAFE_style
|
||||
}: IconProps) {
|
||||
const [svg, setSvg] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof _IconObject[icon] === 'object') {
|
||||
setSvg(_IconObject[icon].ReactComponent);
|
||||
} else {
|
||||
fetch(_IconObject[icon])
|
||||
.then((res) => res.text())
|
||||
.then(setSvg);
|
||||
}
|
||||
}, [icon]);
|
||||
|
||||
if (typeof svg === 'string') {
|
||||
return (
|
||||
<span
|
||||
className={classNames(css['Root'], css[size], variant && css[`is-variant-${variant}`], UNSAFE_className)}
|
||||
style={UNSAFE_style}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const SvgTag = svg;
|
||||
return (
|
||||
<span
|
||||
className={classNames(css['Root'], css[size], variant && css[`is-variant-${variant}`], UNSAFE_className)}
|
||||
style={UNSAFE_style}
|
||||
>
|
||||
{SvgTag && <SvgTag />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Icon'
|
||||
@@ -0,0 +1,23 @@
|
||||
.Root {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.is-variant-grayscale {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.is-size-small {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
&.is-size-large {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { Logo, LogoVariant } from "./Logo";
|
||||
|
||||
export default {
|
||||
title: "Common/Logo",
|
||||
component: Logo,
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof Logo>;
|
||||
|
||||
const Template: ComponentStory<typeof Logo> = (args) => (
|
||||
<div style={{ padding: '10px' }}>
|
||||
<Logo {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
|
||||
export const Inverted = Template.bind({});
|
||||
Inverted.args = {
|
||||
variant: LogoVariant.Inverted
|
||||
};
|
||||
|
||||
export const Grayscale = Template.bind({});
|
||||
Grayscale.args = {
|
||||
variant: LogoVariant.Grayscale
|
||||
};
|
||||
111
packages/noodl-core-ui/src/components/common/Logo/Logo.tsx
Normal file
111
packages/noodl-core-ui/src/components/common/Logo/Logo.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import css from './Logo.module.scss';
|
||||
|
||||
export enum LogoVariant {
|
||||
Default = 'default',
|
||||
Inverted = 'inverted',
|
||||
Grayscale = 'grayscale'
|
||||
}
|
||||
|
||||
export enum LogoSize {
|
||||
Small = 'small',
|
||||
Medium = 'medium',
|
||||
Large = 'large'
|
||||
}
|
||||
|
||||
export interface LogoProps extends UnsafeStyleProps {
|
||||
variant?: LogoVariant;
|
||||
size?: LogoSize;
|
||||
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function Logo({
|
||||
variant = LogoVariant.Default,
|
||||
size = LogoSize.Medium,
|
||||
onClick,
|
||||
UNSAFE_className,
|
||||
UNSAFE_style
|
||||
}: LogoProps) {
|
||||
function VariantIcon(props: {}) {
|
||||
switch (variant) {
|
||||
default:
|
||||
case LogoVariant.Default:
|
||||
return <DefaultIcon />;
|
||||
case LogoVariant.Inverted:
|
||||
return <InvertedIcon />;
|
||||
case LogoVariant.Grayscale:
|
||||
return <GrayscaleIcon />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames([
|
||||
css['Root'],
|
||||
css[`is-variant-${variant}`],
|
||||
css[`is-size-${size}`],
|
||||
UNSAFE_className
|
||||
])}
|
||||
onClick={onClick}
|
||||
style={UNSAFE_style}
|
||||
>
|
||||
<VariantIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DefaultIcon = React.memo(function () {
|
||||
return (
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M30 60C46.5685 60 60 46.5686 60 30C60 13.4315 46.5685 0 30 0C13.4315 0 0 13.4315 0 30C0 46.5686 13.4315 60 30 60Z"
|
||||
fill="#F5BC41"
|
||||
/>
|
||||
<path
|
||||
d="M48.9051 22.4621C48.9051 19.0553 46.8158 16.1367 43.8486 14.9172L43.567 14.8055L44.4605 13.2582C44.7184 12.8117 44.3962 12.2541 43.8804 12.2541H35.9965C35.4813 12.2541 35.1592 12.8123 35.4164 13.2582L39.3584 20.0856C39.6163 20.532 40.2607 20.532 40.5179 20.0856L41.5107 18.3659L42.4474 18.7749C43.8598 19.4112 44.8426 20.8242 44.8426 22.4658C44.8426 23.0909 44.7003 23.6822 44.4461 24.2111L35.1904 44.0383H35.191C35.1079 44.2112 34.9856 44.3705 34.8245 44.5022C34.3312 44.9062 33.6056 44.9056 33.1123 44.501C33.0411 44.4429 32.9781 44.3792 32.9219 44.3112L25.2553 34.9118L23.1141 38.6857L29.9059 47.0299C30.8994 48.1588 32.3549 48.8707 33.9771 48.8707C36.2088 48.8707 38.1258 47.5225 38.9575 45.5956L48.1358 25.9264C48.6291 24.8749 48.9051 23.7009 48.9051 22.4621Z"
|
||||
fill="#1F1F1F"
|
||||
/>
|
||||
<path
|
||||
d="M16.2933 39.3787C16.2833 39.3787 16.2733 39.3781 16.2633 39.3781C14.0142 39.3781 12.1909 41.2014 12.1909 43.4506C12.1909 45.6997 14.0142 47.523 16.2633 47.523C18.5125 47.523 20.3358 45.6997 20.3358 43.4506C20.3358 42.7131 20.1397 42.0213 19.7969 41.425L28.7024 25.0127L28.7037 25.0133C28.7168 24.9902 28.7293 24.9665 28.7417 24.9434L28.7486 24.9322C29.4792 23.5878 29.8944 22.0473 29.8944 20.4095C29.8944 15.1712 25.6477 10.9246 20.4095 10.9246C15.1712 10.9246 10.9246 15.1712 10.9246 20.4095C10.9246 22.7636 11.7819 24.9172 13.2018 26.5756L18.0415 32.5039L20.1957 28.7573L16.264 23.9475C15.4816 23.0077 14.9758 21.7289 14.9758 20.4101C14.9758 17.416 17.4029 14.9889 20.397 14.9889C23.3911 14.9889 25.8182 17.416 25.8182 20.4101C25.8182 21.3792 25.5641 22.289 25.1182 23.0764L25.1157 23.0808L16.2933 39.3787Z"
|
||||
fill="#1F1F1F"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
|
||||
const InvertedIcon = React.memo(function () {
|
||||
return (
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M30 60C46.5685 60 60 46.5685 60 30C60 13.4315 46.5685 0 30 0C13.4315 0 0 13.4315 0 30C0 46.5685 13.4315 60 30 60Z"
|
||||
fill="#1F1F1F"
|
||||
/>
|
||||
<path
|
||||
d="M48.9051 22.462C48.9051 19.0552 46.8158 16.1367 43.8486 14.9172L43.567 14.8054L44.4605 13.2581C44.7184 12.8116 44.3962 12.254 43.8804 12.254H35.9965C35.4814 12.254 35.1592 12.8123 35.4164 13.2581L39.3584 20.0855C39.6163 20.532 40.2607 20.532 40.5179 20.0855L41.5107 18.3658L42.4474 18.7748C43.8598 19.4111 44.8427 20.8242 44.8427 22.4658C44.8427 23.0908 44.7003 23.6822 44.4461 24.211L35.1904 44.0382H35.191C35.108 44.2112 34.9856 44.3704 34.8245 44.5022C34.3312 44.9062 33.6056 44.9055 33.1123 44.5009C33.0411 44.4428 32.9781 44.3792 32.9219 44.3111L25.2553 34.9117L23.1141 38.6857L29.9059 47.0298C30.8994 48.1588 32.3549 48.8706 33.9771 48.8706C36.2088 48.8706 38.1258 47.5225 38.9575 45.5955L48.1358 25.9263C48.6291 24.8748 48.9051 23.7009 48.9051 22.462Z"
|
||||
fill="#F5BC41"
|
||||
/>
|
||||
<path
|
||||
d="M16.2933 39.3787C16.2833 39.3787 16.2733 39.3781 16.2633 39.3781C14.0142 39.3781 12.1909 41.2014 12.1909 43.4506C12.1909 45.6997 14.0142 47.523 16.2633 47.523C18.5125 47.523 20.3358 45.6997 20.3358 43.4506C20.3358 42.7131 20.1397 42.0213 19.7969 41.425L28.7024 25.0127L28.7037 25.0133C28.7168 24.9902 28.7293 24.9665 28.7417 24.9434L28.7486 24.9322C29.4792 23.5878 29.8944 22.0473 29.8944 20.4095C29.8944 15.1712 25.6477 10.9246 20.4095 10.9246C15.1712 10.9246 10.9246 15.1712 10.9246 20.4095C10.9246 22.7636 11.7819 24.9172 13.2018 26.5756L18.0415 32.5039L20.1957 28.7573L16.264 23.9475C15.4816 23.0077 14.9758 21.7289 14.9758 20.4101C14.9758 17.416 17.4029 14.9889 20.397 14.9889C23.3911 14.9889 25.8182 17.416 25.8182 20.4101C25.8182 21.3792 25.5641 22.289 25.1182 23.0764L25.1157 23.0808L16.2933 39.3787Z"
|
||||
fill="#F5BC41"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
|
||||
const GrayscaleIcon = React.memo(function () {
|
||||
return (
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M48.9051 22.462C48.9051 19.0552 46.8158 16.1367 43.8486 14.9172L43.567 14.8054L44.4605 13.2581C44.7184 12.8116 44.3962 12.254 43.8804 12.254H35.9965C35.4814 12.254 35.1592 12.8123 35.4164 13.2581L39.3584 20.0855C39.6163 20.532 40.2607 20.532 40.5179 20.0855L41.5107 18.3658L42.4474 18.7748C43.8598 19.4111 44.8427 20.8242 44.8427 22.4658C44.8427 23.0908 44.7003 23.6822 44.4461 24.211L35.1904 44.0382H35.191C35.108 44.2112 34.9856 44.3704 34.8245 44.5022C34.3312 44.9062 33.6056 44.9055 33.1123 44.5009C33.0411 44.4428 32.9781 44.3792 32.9219 44.3111L25.2553 34.9117L23.1141 38.6857L29.9059 47.0298C30.8994 48.1588 32.3549 48.8706 33.9771 48.8706C36.2088 48.8706 38.1258 47.5225 38.9575 45.5955L48.1358 25.9263C48.6291 24.8748 48.9051 23.7009 48.9051 22.462Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M16.2933 39.3787C16.2833 39.3787 16.2733 39.3781 16.2633 39.3781C14.0142 39.3781 12.1909 41.2014 12.1909 43.4506C12.1909 45.6997 14.0142 47.523 16.2633 47.523C18.5125 47.523 20.3358 45.6997 20.3358 43.4506C20.3358 42.7131 20.1397 42.0213 19.7969 41.425L28.7024 25.0127L28.7037 25.0133C28.7168 24.9902 28.7293 24.9665 28.7417 24.9434L28.7486 24.9322C29.4792 23.5878 29.8944 22.0473 29.8944 20.4095C29.8944 15.1712 25.6477 10.9246 20.4095 10.9246C15.1712 10.9246 10.9246 15.1712 10.9246 20.4095C10.9246 22.7636 11.7819 24.9172 13.2018 26.5756L18.0415 32.5039L20.1957 28.7573L16.264 23.9475C15.4816 23.0077 14.9758 21.7289 14.9758 20.4101C14.9758 17.416 17.4029 14.9889 20.397 14.9889C23.3911 14.9889 25.8182 17.416 25.8182 20.4101C25.8182 21.3792 25.5641 22.289 25.1182 23.0764L25.1157 23.0808L16.2933 39.3787Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Logo';
|
||||
@@ -0,0 +1,57 @@
|
||||
.Root {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
font-family: var(--font-family);
|
||||
font-size: 12px;
|
||||
color: var(--theme-color-fg-default);
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-family);
|
||||
color: var(--theme-color-fg-default);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: var(--font-family);
|
||||
color: var(--theme-color-fg-default);
|
||||
font-size: 12px;
|
||||
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 700;
|
||||
color: var(--theme-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
display: block;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
margin-inline-start: 16px;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Markdown } from './Markdown';
|
||||
|
||||
export default {
|
||||
title: 'Common/Markdown',
|
||||
component: Markdown,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof Markdown>;
|
||||
|
||||
const Template: ComponentStory<typeof Markdown> = (args) => <Markdown {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
|
||||
export const TextSpanStyle = Template.bind({});
|
||||
TextSpanStyle.args = {
|
||||
content: `
|
||||
<span style="color: red;">Red Text</span>
|
||||
<span style="color: green;">Green Text</span>
|
||||
<span style="color: blue;">Blue Text</span>
|
||||
`
|
||||
};
|
||||
|
||||
export const Demo = Template.bind({});
|
||||
Demo.args = {
|
||||
content: `
|
||||
# h1 Heading 8-)
|
||||
## h2 Heading
|
||||
### h3 Heading
|
||||
#### h4 Heading
|
||||
##### h5 Heading
|
||||
###### h6 Heading
|
||||
|
||||
|
||||
## Horizontal Rules
|
||||
|
||||
___
|
||||
|
||||
---
|
||||
|
||||
***
|
||||
|
||||
|
||||
## Typographic replacements
|
||||
|
||||
Enable typographer option to see result.
|
||||
|
||||
(c) (C) (r) (R) (tm) (TM) (p) (P) +-
|
||||
|
||||
test.. test... test..... test?..... test!....
|
||||
|
||||
!!!!!! ???? ,, -- ---
|
||||
|
||||
"Smartypants, double quotes" and 'single quotes'
|
||||
|
||||
|
||||
## Emphasis
|
||||
|
||||
**This is bold text**
|
||||
|
||||
__This is bold text__
|
||||
|
||||
*This is italic text*
|
||||
|
||||
_This is italic text_
|
||||
|
||||
~~Strikethrough~~
|
||||
|
||||
|
||||
## Blockquotes
|
||||
|
||||
|
||||
> Blockquotes can also be nested...
|
||||
>> ...by using additional greater-than signs right next to each other...
|
||||
> > > ...or with spaces between arrows.
|
||||
|
||||
|
||||
## Lists
|
||||
|
||||
Unordered
|
||||
|
||||
+ Create a list by starting a line with \`+\`, \`-\`, or \`*\`
|
||||
+ Sub-lists are made by indenting 2 spaces:
|
||||
- Marker character change forces new list start:
|
||||
* Ac tristique libero volutpat at
|
||||
+ Facilisis in pretium nisl aliquet
|
||||
- Nulla volutpat aliquam velit
|
||||
+ Very easy!
|
||||
|
||||
Ordered
|
||||
|
||||
1. Lorem ipsum dolor sit amet
|
||||
2. Consectetur adipiscing elit
|
||||
3. Integer molestie lorem at massa
|
||||
|
||||
|
||||
1. You can use sequential numbers...
|
||||
1. ...or keep all the numbers as \`1.\`
|
||||
|
||||
Start numbering with offset:
|
||||
|
||||
57. foo
|
||||
1. bar
|
||||
|
||||
|
||||
## Code
|
||||
|
||||
Inline \`code\`
|
||||
|
||||
Indented code
|
||||
|
||||
// Some comments
|
||||
line 1 of code
|
||||
line 2 of code
|
||||
line 3 of code
|
||||
|
||||
|
||||
Block code "fences"
|
||||
|
||||
\`\`\`
|
||||
Sample text here...
|
||||
\`\`\`
|
||||
|
||||
Syntax highlighting
|
||||
|
||||
\`\`\`js
|
||||
var foo = function (bar) {
|
||||
return bar++;
|
||||
};
|
||||
|
||||
console.log(foo(5));
|
||||
\`\`\`
|
||||
|
||||
## Tables
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
| data | path to data files to supply the data that will be passed into templates. |
|
||||
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||
| ext | extension to be used for dest files. |
|
||||
|
||||
Right aligned columns
|
||||
|
||||
| Option | Description |
|
||||
| ------:| -----------:|
|
||||
| data | path to data files to supply the data that will be passed into templates. |
|
||||
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||
| ext | extension to be used for dest files. |
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
[link text](http://dev.nodeca.com)
|
||||
|
||||
[link with title](http://nodeca.github.io/pica/demo/ "title text!")
|
||||
|
||||
Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
|
||||
|
||||
|
||||
## Images
|
||||
|
||||

|
||||

|
||||
|
||||
Like links, Images also have a footnote style syntax
|
||||
|
||||
![Alt text][id]
|
||||
|
||||
With a reference later in the document defining the URL location:
|
||||
|
||||
[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
|
||||
|
||||
|
||||
## Plugins
|
||||
|
||||
The killer feature of \`markdown-it\` is very effective support of
|
||||
[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin).
|
||||
|
||||
|
||||
### [Emojies](https://github.com/markdown-it/markdown-it-emoji)
|
||||
|
||||
> Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum:
|
||||
>
|
||||
> Shortcuts (emoticons): :-) :-( 8-) ;)
|
||||
|
||||
see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji.
|
||||
|
||||
|
||||
### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)
|
||||
|
||||
- 19^th^
|
||||
- H~2~O
|
||||
|
||||
|
||||
### [\<ins>](https://github.com/markdown-it/markdown-it-ins)
|
||||
|
||||
++Inserted text++
|
||||
|
||||
|
||||
### [\<mark>](https://github.com/markdown-it/markdown-it-mark)
|
||||
|
||||
==Marked text==
|
||||
|
||||
|
||||
### [Footnotes](https://github.com/markdown-it/markdown-it-footnote)
|
||||
|
||||
Footnote 1 link[^first].
|
||||
|
||||
Footnote 2 link[^second].
|
||||
|
||||
Inline footnote^[Text of inline footnote] definition.
|
||||
|
||||
Duplicated footnote reference[^second].
|
||||
|
||||
[^first]: Footnote **can have markup**
|
||||
|
||||
and multiple paragraphs.
|
||||
|
||||
[^second]: Footnote text.
|
||||
|
||||
|
||||
### [Definition lists](https://github.com/markdown-it/markdown-it-deflist)
|
||||
|
||||
Term 1
|
||||
|
||||
: Definition 1
|
||||
with lazy continuation.
|
||||
|
||||
Term 2 with *inline markup*
|
||||
|
||||
: Definition 2
|
||||
|
||||
{ some code, part of Definition 2 }
|
||||
|
||||
Third paragraph of definition 2.
|
||||
|
||||
_Compact style:_
|
||||
|
||||
Term 1
|
||||
~ Definition 1
|
||||
|
||||
Term 2
|
||||
~ Definition 2a
|
||||
~ Definition 2b
|
||||
|
||||
|
||||
### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr)
|
||||
|
||||
This is HTML abbreviation example.
|
||||
|
||||
It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on.
|
||||
|
||||
*[HTML]: Hyper Text Markup Language
|
||||
|
||||
### [Custom containers](https://github.com/markdown-it/markdown-it-container)
|
||||
|
||||
::: warning
|
||||
*here be dragons*
|
||||
:::
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import classNames from 'classnames';
|
||||
import { Remarkable } from 'remarkable';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './Markdown.module.scss';
|
||||
|
||||
export interface MarkdownProps extends UnsafeStyleProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function Markdown({ content, UNSAFE_className, UNSAFE_style }: MarkdownProps) {
|
||||
const __html = useMemo(() => {
|
||||
const md = new Remarkable({
|
||||
html: true,
|
||||
breaks: true
|
||||
});
|
||||
|
||||
return md.render(content);
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames([css['Root'], UNSAFE_className])}
|
||||
style={UNSAFE_style}
|
||||
dangerouslySetInnerHTML={{ __html }}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Markdown';
|
||||
@@ -0,0 +1,151 @@
|
||||
.Root {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 64px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 0;
|
||||
background-color: var(--theme-color-bg-2);
|
||||
text-align: left;
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
font-family: var(--font-family);
|
||||
|
||||
&:not(.has-no-bottom-border) {
|
||||
border-bottom: 1px solid var(--theme-color-bg-1);
|
||||
}
|
||||
|
||||
.Label {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: var(--theme-color-fg-default);
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--theme-color-fg-default-shy);
|
||||
}
|
||||
|
||||
&.is-inactive {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.has-top-divider {
|
||||
border-top: 1px solid var(--theme-color-bg-1);
|
||||
}
|
||||
|
||||
&:hover:not(&.is-inactive) {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
|
||||
&.is-variant-background {
|
||||
padding-right: 18px;
|
||||
cursor: progress;
|
||||
background-color: var(--theme-color-bg-2);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-2);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-background-action {
|
||||
padding-right: 18px;
|
||||
cursor: progress;
|
||||
background-color: var(--theme-color-bg-3);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-4) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-user-action {
|
||||
background-color: var(--theme-color-primary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-primary-highlight) !important;
|
||||
}
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-on-primary);
|
||||
}
|
||||
|
||||
p,
|
||||
small {
|
||||
color: var(--theme-color-on-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-downloading {
|
||||
&:hover {
|
||||
background-color: var(--theme-color-secondary-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-proud {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
|
||||
&.is-variant-on-bg-2 {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-4) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-secondary {
|
||||
background-color: var(--theme-color-secondary-dim);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-secondary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
padding: 16px;
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.Text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding-right: 8px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
small {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { ActionButton, ActionButtonVariant } from './ActionButton';
|
||||
import { IconName } from '@noodl-core-ui/components/common/Icon';
|
||||
import { Text, TextType } from '@noodl-core-ui/components/typography/Text';
|
||||
import { Container } from '@noodl-core-ui/components/layout/Container';
|
||||
|
||||
export default {
|
||||
title: 'Inputs/Action Button',
|
||||
component: ActionButton,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof ActionButton>;
|
||||
|
||||
const Template: ComponentStory<typeof ActionButton> = (args) => (
|
||||
<div style={{ width: 280 }}>
|
||||
<ActionButton {...args}></ActionButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
|
||||
export const UpToDate = Template.bind({});
|
||||
UpToDate.args = {
|
||||
variant: ActionButtonVariant.Default,
|
||||
label: 'Up to date',
|
||||
value: 'Last updated 14:39'
|
||||
};
|
||||
|
||||
export const ReceivingUpdates = Template.bind({});
|
||||
ReceivingUpdates.args = {
|
||||
variant: ActionButtonVariant.BackgroundAction,
|
||||
label: 'Receiving updates',
|
||||
affixText: '75%'
|
||||
};
|
||||
|
||||
export const CheckingForUpdates = Template.bind({});
|
||||
CheckingForUpdates.args = {
|
||||
variant: ActionButtonVariant.BackgroundAction,
|
||||
label: 'Checking for updates...',
|
||||
affixText: 'Last updated 14:39'
|
||||
};
|
||||
|
||||
export const PullChanges = Template.bind({});
|
||||
PullChanges.args = {
|
||||
variant: ActionButtonVariant.CallToAction,
|
||||
icon: IconName.ArrowDown,
|
||||
label: 'Pull changes',
|
||||
affixText: 'Last updates just now'
|
||||
};
|
||||
|
||||
export const PushChanges = Template.bind({});
|
||||
PushChanges.args = {
|
||||
variant: ActionButtonVariant.CallToAction,
|
||||
icon: IconName.ArrowUp,
|
||||
label: 'Push changes',
|
||||
affixText: 'Last updates just now'
|
||||
};
|
||||
|
||||
export const Back = Template.bind({});
|
||||
Back.args = {
|
||||
variant: ActionButtonVariant.Default,
|
||||
icon: IconName.ArrowLeft,
|
||||
label: 'Back',
|
||||
affixText: undefined
|
||||
};
|
||||
|
||||
export const ComparingBranches = Template.bind({});
|
||||
ComparingBranches.args = {
|
||||
variant: ActionButtonVariant.Proud,
|
||||
icon: IconName.ArrowLeft,
|
||||
prefixText: 'Comparing',
|
||||
label: (
|
||||
<Container>
|
||||
<Text textType={TextType.Proud} isSpan>
|
||||
Branch v2
|
||||
</Text>
|
||||
<Text textType={TextType.Default} isSpan style={{ padding: '0 4px' }}>
|
||||
with
|
||||
</Text>
|
||||
<Text textType={TextType.Proud} isSpan>
|
||||
Main
|
||||
</Text>
|
||||
</Container>
|
||||
),
|
||||
affixText: undefined
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { FocusEventHandler, MouseEventHandler, useMemo } from 'react';
|
||||
|
||||
import { ActivityIndicator } from '@noodl-core-ui/components/common/ActivityIndicator';
|
||||
import { Icon, IconName, IconVariant } from '@noodl-core-ui/components/common/Icon';
|
||||
import { TextType } from '@noodl-core-ui/components/typography/Text';
|
||||
import { Slot } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './ActionButton.module.scss';
|
||||
|
||||
export enum ActionButtonVariant {
|
||||
Default = 'default',
|
||||
Secondary = 'secondary',
|
||||
OnBg2 = 'on-bg-2',
|
||||
/** CallToAction is when the user __can__ act, but it is doing something. */
|
||||
CallToAction = 'user-action',
|
||||
Background = 'background',
|
||||
/** BackgroundAction is when the user __can't__ act */
|
||||
BackgroundAction = 'background-action',
|
||||
Proud = 'proud'
|
||||
}
|
||||
|
||||
export interface ActionButtonProps {
|
||||
variant?: ActionButtonVariant;
|
||||
icon?: IconName;
|
||||
affixIcon?: IconName;
|
||||
|
||||
prefixText?: string;
|
||||
label: string | Slot;
|
||||
affixText?: string;
|
||||
|
||||
hasTopDivider?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isInactive?: boolean;
|
||||
|
||||
hasNoBottomBorder?: boolean;
|
||||
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
onMouseEnter?: MouseEventHandler<HTMLButtonElement>;
|
||||
onMouseLeave?: MouseEventHandler<HTMLButtonElement>;
|
||||
onFocus?: FocusEventHandler<HTMLButtonElement>;
|
||||
onBlur?: FocusEventHandler<HTMLButtonElement>;
|
||||
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function ActionButton({
|
||||
variant = ActionButtonVariant.Default,
|
||||
icon = IconName.Refresh,
|
||||
affixIcon,
|
||||
|
||||
prefixText,
|
||||
label,
|
||||
affixText,
|
||||
|
||||
hasTopDivider,
|
||||
isDisabled = false,
|
||||
hasNoBottomBorder,
|
||||
isInactive,
|
||||
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onFocus,
|
||||
onBlur,
|
||||
|
||||
testId
|
||||
}: ActionButtonProps) {
|
||||
const iconVariant: IconVariant = useMemo(() => {
|
||||
switch (variant) {
|
||||
default:
|
||||
return TextType.Default;
|
||||
case ActionButtonVariant.CallToAction:
|
||||
return TextType.Shy;
|
||||
}
|
||||
}, [variant]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames([
|
||||
css['Root'],
|
||||
css[`is-variant-${variant}`],
|
||||
hasNoBottomBorder && css['has-no-bottom-border'],
|
||||
isInactive && css['is-inactive'],
|
||||
hasTopDivider && css['has-top-divider']
|
||||
])}
|
||||
onClick={(e) => {
|
||||
if (onClick) onClick(e);
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
disabled={isDisabled}
|
||||
data-test={testId}
|
||||
>
|
||||
<div className={classNames(css['Container'])}>
|
||||
<div className={classNames(css['Icon'], isDisabled && css['is-disabled'])}>
|
||||
<Icon icon={icon} />
|
||||
</div>
|
||||
<div className={classNames(css['Text'], isDisabled && css['is-disabled'])}>
|
||||
{Boolean(prefixText) && <small className={css['Label']}>{prefixText}</small>}
|
||||
{Boolean(typeof label === 'string') ? <p className={css['Label']}>{label}</p> : label}
|
||||
{Boolean(affixText) && <small className={css['Label']}>{affixText}</small>}
|
||||
</div>
|
||||
{Boolean(affixIcon) && (
|
||||
<div className={classNames(css['Icon'], isDisabled && css['is-disabled'])}>
|
||||
<Icon icon={affixIcon} variant={iconVariant} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{variant === ActionButtonVariant.BackgroundAction && <ActivityIndicator />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ActionButton';
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Dispatch, useState } from 'react';
|
||||
|
||||
export function useTogglableCheckboxes<T = string | number>(): [
|
||||
selectedCheckboxes: T[],
|
||||
getIsChecked: (value: T) => boolean,
|
||||
toggleCheckbox: (value: T) => void,
|
||||
clearSelected: () => void
|
||||
] {
|
||||
const [selectedCheckboxes, setSelectedCheckboxes] = useState<T[]>([]);
|
||||
|
||||
function toggleCheckbox(value: T) {
|
||||
setSelectedCheckboxes((prev) => {
|
||||
if (prev.includes(value)) {
|
||||
return [...prev].filter((arrValue) => arrValue !== value);
|
||||
} else {
|
||||
return [...prev, value];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getIsChecked(value: T) {
|
||||
return selectedCheckboxes.includes(value);
|
||||
}
|
||||
|
||||
function clearSelected() {
|
||||
setSelectedCheckboxes([]);
|
||||
}
|
||||
|
||||
return [selectedCheckboxes, getIsChecked, toggleCheckbox, clearSelected];
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
.Root {
|
||||
font-family: var(--font-family);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.has-bottom-spacing {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.Checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.FauxCheckbox {
|
||||
background-color: var(--theme-color-bg-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.is-size-small {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.is-size-large {
|
||||
// Placeholder styles here atm, should match the look of checkboxes in the Property Panel
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
&.has-right-margin {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.Root.is-variant-sidebar & {
|
||||
background-color: var(--theme-color-bg-1);
|
||||
}
|
||||
|
||||
.Root.is-variant-light & {
|
||||
background-color: var(--theme-color-bg-4);
|
||||
}
|
||||
}
|
||||
|
||||
.CheckMark {
|
||||
transition: opacity var(--speed-turbo);
|
||||
pointer-events: none;
|
||||
|
||||
&.is-checked {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.is-unchecked {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.Label {
|
||||
padding-left: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--theme-color-fg-default);
|
||||
transition: color var(--speed-turbo);
|
||||
|
||||
.Root:hover & {
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
.ChildContainer {
|
||||
padding-left: 16px;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Checkbox, CheckboxSize } from './Checkbox';
|
||||
|
||||
export default {
|
||||
title: 'Inputs/Checkbox',
|
||||
component: Checkbox,
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof Checkbox>;
|
||||
|
||||
const Template: ComponentStory<typeof Checkbox> = (args) => <Checkbox {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
|
||||
export const Selected = Template.bind({});
|
||||
Selected.args = {
|
||||
label: "I want cookies",
|
||||
isChecked: true
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
label: "I want cookies",
|
||||
isDisabled: true,
|
||||
};
|
||||
|
||||
export const HiddenCheckbox = Template.bind({});
|
||||
HiddenCheckbox.args = {
|
||||
label: "I want cookies",
|
||||
hasHiddenCheckbox: true,
|
||||
};
|
||||
|
||||
export const SizeSmall = Template.bind({});
|
||||
SizeSmall.args = {
|
||||
label: "I want cookies",
|
||||
isChecked: true,
|
||||
checkboxSize: CheckboxSize.Small,
|
||||
};
|
||||
|
||||
export const SizeLarge = Template.bind({});
|
||||
SizeLarge.args = {
|
||||
label: "I want cookies",
|
||||
isChecked: true,
|
||||
checkboxSize: CheckboxSize.Large,
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ChangeEventHandler, cloneElement, FocusEventHandler, MouseEventHandler } from 'react';
|
||||
|
||||
import { InputNotification } from '@noodl-types/globalInputTypes';
|
||||
|
||||
import { ReactComponent as CheckmarkIcon } from '@noodl-core-ui/assets/icons/checkmark.svg';
|
||||
import { Slot, UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
|
||||
import { InputLabelSection } from '../InputLabelSection';
|
||||
import { NotificationFeedbackDisplay } from '../NotificationFeedbackDisplay';
|
||||
import { useNotificationFeedbackDisplay } from '../NotificationFeedbackDisplay/NotificationFeedbackDisplay.hooks';
|
||||
import css from './Checkbox.module.scss';
|
||||
|
||||
export enum CheckboxVariant {
|
||||
Default = 'default',
|
||||
Sidebar = 'sidebar',
|
||||
Light = 'light'
|
||||
}
|
||||
|
||||
export enum CheckboxSize {
|
||||
Small = 'small',
|
||||
Large = 'large'
|
||||
}
|
||||
|
||||
export interface CheckboxProps extends UnsafeStyleProps {
|
||||
variant?: CheckboxVariant;
|
||||
|
||||
label?: string;
|
||||
value?: string | number;
|
||||
children?: Slot;
|
||||
checkboxSize?: CheckboxSize;
|
||||
notification?: InputNotification;
|
||||
|
||||
hasHiddenCheckbox?: boolean;
|
||||
hasBottomSpacing?: boolean;
|
||||
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
onMouseEnter?: MouseEventHandler<HTMLLabelElement>;
|
||||
onMouseLeave?: MouseEventHandler<HTMLLabelElement>;
|
||||
onFocus?: FocusEventHandler<HTMLLabelElement>;
|
||||
onBlur?: FocusEventHandler<HTMLLabelElement>;
|
||||
|
||||
isChecked?: boolean;
|
||||
isDisabled?: boolean;
|
||||
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function Checkbox({
|
||||
variant = CheckboxVariant.Default,
|
||||
|
||||
label,
|
||||
value,
|
||||
children,
|
||||
checkboxSize = CheckboxSize.Small,
|
||||
notification,
|
||||
|
||||
hasHiddenCheckbox,
|
||||
hasBottomSpacing,
|
||||
|
||||
onChange,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onFocus,
|
||||
onBlur,
|
||||
|
||||
isChecked,
|
||||
isDisabled,
|
||||
|
||||
testId,
|
||||
|
||||
UNSAFE_className,
|
||||
UNSAFE_style
|
||||
}: CheckboxProps) {
|
||||
const [newNotification, setNewNotification] = useNotificationFeedbackDisplay(notification);
|
||||
|
||||
return (
|
||||
<label
|
||||
className={classNames([
|
||||
css['Root'],
|
||||
css[`is-variant-${variant}`],
|
||||
isDisabled && css['is-disabled'],
|
||||
hasBottomSpacing && css['has-bottom-spacing'],
|
||||
UNSAFE_className
|
||||
])}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
data-test={testId}
|
||||
style={UNSAFE_style}
|
||||
>
|
||||
<NotificationFeedbackDisplay notification={newNotification} />
|
||||
|
||||
<input
|
||||
className={css['Checkbox']}
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
|
||||
{!hasHiddenCheckbox && (
|
||||
<div
|
||||
className={classNames([
|
||||
css['FauxCheckbox'],
|
||||
css[`is-size-${checkboxSize}`],
|
||||
Boolean(label) && css['has-right-margin']
|
||||
])}
|
||||
>
|
||||
{isChecked && <CheckmarkIcon />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children && <div className={css['ChildContainer']}>{cloneElement(children as TSFixme, { isChecked })}</div>}
|
||||
{label && <InputLabelSection label={label} />}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Checkbox';
|
||||
@@ -0,0 +1,17 @@
|
||||
.Root {
|
||||
color: var(--theme-color-fg-highlight);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: top;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
.Icon {
|
||||
margin-left: 6px;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { ExternalLink } from './ExternalLink';
|
||||
|
||||
export default {
|
||||
title: 'Inputs/External Link',
|
||||
component: ExternalLink,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof ExternalLink>;
|
||||
|
||||
const Template: ComponentStory<typeof ExternalLink> = (args) => <ExternalLink {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = { children: 'I am a link' };
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import css from './ExternalLink.module.scss';
|
||||
import { platform } from '@noodl/platform';
|
||||
import useParsedHref from '@noodl-hooks/useParsedHref';
|
||||
import classNames from 'classnames';
|
||||
import { Slot, UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
|
||||
|
||||
export interface ExternalLinkProps extends UnsafeStyleProps {
|
||||
children: Slot;
|
||||
href: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function ExternalLink({
|
||||
children,
|
||||
href,
|
||||
testId,
|
||||
UNSAFE_className,
|
||||
UNSAFE_style
|
||||
}: ExternalLinkProps) {
|
||||
const parsedHref = useParsedHref(href);
|
||||
function handleClick() {
|
||||
platform.openExternal(parsedHref);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className={classNames(css['Root'], UNSAFE_className)}
|
||||
onClick={handleClick}
|
||||
target="_blank"
|
||||
data-test={testId}
|
||||
style={UNSAFE_style}
|
||||
>
|
||||
{children}
|
||||
<Icon UNSAFE_className={css['Icon']} icon={IconName.ExternalLink} size={IconSize.Tiny} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ExternalLink';
|
||||
@@ -0,0 +1,122 @@
|
||||
.Root {
|
||||
padding: 4px;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--speed-turbo) var(--easing-base);
|
||||
|
||||
path {
|
||||
transition: fill var(--speed-turbo) var(--easing-base);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--theme-color-bg-2);
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-default-shy);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-default {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-default);
|
||||
}
|
||||
|
||||
&.is-state-active,
|
||||
&:not(:disabled):hover {
|
||||
background-color: var(--theme-color-bg-1);
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-semi-transparent {
|
||||
background-color: var(--theme-color-fg-transparent);
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-highlight);
|
||||
}
|
||||
|
||||
&.is-state-active,
|
||||
&:not(:disabled):hover {
|
||||
background-color: var(--theme-color-bg-1-transparent);
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-transparent {
|
||||
background-color: transparent !important;
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-default);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
path {
|
||||
fill: var(--theme-color-bg-4);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-state-active,
|
||||
&:not(:disabled):hover {
|
||||
background-color: var(--theme-color-bg-1-transparent);
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-variant-opaque-on-hover {
|
||||
background-color: transparent;
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-default);
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
|
||||
path {
|
||||
fill: var(--theme-color-fg-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
path {
|
||||
fill: var(--theme-color-bg-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-button-size-bigger {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.Icon {
|
||||
transition: transform var(--speed-quick) var(--easing-base);
|
||||
|
||||
.Root.is-state-rotated & {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.Root.is-icon-variant-notice:hover & {
|
||||
path {
|
||||
fill: var(--theme-color-primary-highlight) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Label {
|
||||
margin-left: 4px;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { IconButton } from './IconButton';
|
||||
|
||||
export default {
|
||||
title: 'Inputs/Icon Button',
|
||||
component: IconButton,
|
||||
argTypes: {}
|
||||
} as ComponentMeta<typeof IconButton>;
|
||||
|
||||
const Template: ComponentStory<typeof IconButton> = (args) => (
|
||||
<>
|
||||
<IconButton {...args} />
|
||||
</>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,102 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { MouseEventHandler } from 'react';
|
||||
|
||||
import { Icon, IconName, IconSize, IconVariant } from '@noodl-core-ui/components/common/Icon';
|
||||
import { Label, LabelSize } from '@noodl-core-ui/components/typography/Label';
|
||||
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
|
||||
|
||||
import css from './IconButton.module.scss';
|
||||
|
||||
export enum IconButtonVariant {
|
||||
Default = 'is-variant-default',
|
||||
Transparent = 'is-variant-transparent',
|
||||
SemiTransparent = 'is-variant-semi-transparent',
|
||||
OpaqueOnHover = 'is-variant-opaque-on-hover'
|
||||
}
|
||||
|
||||
export enum IconButtonState {
|
||||
Default = 'is-state-default',
|
||||
Active = 'is-state-active',
|
||||
Rotated = 'is-state-rotated'
|
||||
}
|
||||
|
||||
export enum IconButtonSize {
|
||||
Default = 'is-button-size-default',
|
||||
Bigger = 'is-button-size-bigger'
|
||||
}
|
||||
|
||||
export interface IconButtonProps extends UnsafeStyleProps {
|
||||
icon: IconName;
|
||||
size?: IconSize;
|
||||
buttonSize?: IconButtonSize;
|
||||
variant?: IconButtonVariant;
|
||||
state?: IconButtonState;
|
||||
iconVariant?: IconVariant;
|
||||
label?: string;
|
||||
|
||||
isDisabled?: boolean;
|
||||
testId?: string;
|
||||
id?: string;
|
||||
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
function iconSizeToLabelSize(iconSize: IconSize): LabelSize {
|
||||
switch (iconSize) {
|
||||
case IconSize.Large:
|
||||
return LabelSize.Big;
|
||||
case IconSize.Small:
|
||||
return LabelSize.Small;
|
||||
case IconSize.Tiny:
|
||||
return LabelSize.Small;
|
||||
default:
|
||||
return LabelSize.Default;
|
||||
}
|
||||
}
|
||||
|
||||
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
size = IconSize.Default,
|
||||
buttonSize = IconButtonSize.Default,
|
||||
variant = IconButtonVariant.Default,
|
||||
state = IconButtonState.Default,
|
||||
iconVariant,
|
||||
isDisabled,
|
||||
label,
|
||||
testId,
|
||||
onClick,
|
||||
UNSAFE_className,
|
||||
UNSAFE_style,
|
||||
id
|
||||
}: IconButtonProps,
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={classNames(
|
||||
css['Root'],
|
||||
css[variant],
|
||||
css[state],
|
||||
css[buttonSize],
|
||||
css[`is-icon-variant-${iconVariant}`],
|
||||
UNSAFE_className
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={isDisabled}
|
||||
style={UNSAFE_style}
|
||||
data-test={testId}
|
||||
>
|
||||
<Icon icon={icon} size={size} UNSAFE_className={css['Icon']} variant={iconVariant} />
|
||||
{label && (
|
||||
<Label size={iconSizeToLabelSize(size)} variant={iconVariant} UNSAFE_className={css.Label}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './IconButton'
|
||||
@@ -0,0 +1,9 @@
|
||||
.Root {
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
color: var(--theme-color-fg-default);
|
||||
text-align: left;
|
||||
padding-bottom: 3px;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { InputLabelSection } from './InputLabelSection';
|
||||
|
||||
export default {
|
||||
title: 'Inputs/Input Label Section',
|
||||
component: InputLabelSection,
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof InputLabelSection>;
|
||||
|
||||
const Template: ComponentStory<typeof InputLabelSection> = (args) => <InputLabelSection {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {
|
||||
label: 'Hello World',
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import css from './InputLabelSection.module.scss';
|
||||
|
||||
export interface InputLabelSectionProps {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function InputLabelSection({ label }: InputLabelSectionProps) {
|
||||
return <div className={css['Root']}>{label}</div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './InputLabelSection';
|
||||
@@ -0,0 +1,28 @@
|
||||
.Root {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
transition: opacity var(--speed-turbo), background-color var(--speed-turbo);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
}
|
||||
|
||||
.Icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform var(--speed-quick) var(--easing-base);
|
||||
pointer-events: none;
|
||||
|
||||
&.is-rotated-180 {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { LegacyIconButton } from './LegacyIconButton';
|
||||
|
||||
export default {
|
||||
title: 'Inputs/Legacy Icon Button',
|
||||
component: LegacyIconButton,
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof LegacyIconButton>;
|
||||
|
||||
const Template: ComponentStory<typeof LegacyIconButton> = (args) => (
|
||||
<>
|
||||
DONT USE THIS COMPONENT
|
||||
<LegacyIconButton {...args} />
|
||||
</>
|
||||
);
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
@@ -0,0 +1,49 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
MouseEvent,
|
||||
MouseEventHandler,
|
||||
SyntheticEvent,
|
||||
} from 'react';
|
||||
import css from './LegacyIconButton.module.scss';
|
||||
|
||||
export enum LegacyIconButtonIcon {
|
||||
VerticalDots = 'vertical-dots',
|
||||
Close = 'close',
|
||||
CloseDark = 'close-dark',
|
||||
CaretDown = 'caret-down',
|
||||
Generate = 'generate',
|
||||
}
|
||||
|
||||
export interface LegacyIconButtonProps {
|
||||
icon: LegacyIconButtonIcon;
|
||||
isRotated180?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
style?: CSSProperties;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function LegacyIconButton({
|
||||
icon,
|
||||
isRotated180,
|
||||
style,
|
||||
onClick,
|
||||
testId,
|
||||
}: LegacyIconButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={css['Root']}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
data-test={testId}
|
||||
>
|
||||
<img
|
||||
className={classNames([
|
||||
css['Icon'],
|
||||
isRotated180 && css['is-rotated-180'],
|
||||
])}
|
||||
src={`../assets/icons/icon-button/${icon}.svg`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LegacyIconButton';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { InputNotification } from '@noodl-types/globalInputTypes';
|
||||
|
||||
export function useNotificationFeedbackDisplay(initialNotification: InputNotification | null) {
|
||||
const [newNotification, setNewNotification] = useState<InputNotification | null>(null);
|
||||
|
||||
function updateNotification(notificationObject: InputNotification | null) {
|
||||
setNewNotification(null);
|
||||
|
||||
if (notificationObject) {
|
||||
requestAnimationFrame(() => {
|
||||
setNewNotification(notificationObject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateNotification(initialNotification);
|
||||
}, [initialNotification]);
|
||||
|
||||
return [newNotification, updateNotification] as [
|
||||
InputNotification,
|
||||
(notificationObject: InputNotification | null) => void
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.Root {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid;
|
||||
pointer-events: none;
|
||||
|
||||
&.is-type {
|
||||
&-success {
|
||||
border-color: var(--theme-color-success);
|
||||
}
|
||||
|
||||
&-danger {
|
||||
border-color: var(--theme-color-danger);
|
||||
}
|
||||
|
||||
&-notice {
|
||||
border-color: var(--theme-color-notice);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-display-mode {
|
||||
&-fade-quick {
|
||||
opacity: 1;
|
||||
animation: fade 2000ms 200ms forwards;
|
||||
}
|
||||
|
||||
&-fade-slow {
|
||||
opacity: 1;
|
||||
animation: fade 2000ms 3000ms forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { NotificationFeedbackDisplay } from './NotificationFeedbackDisplay';
|
||||
|
||||
export default {
|
||||
title: 'Inputs/Notification Feedback Display',
|
||||
component: NotificationFeedbackDisplay,
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof NotificationFeedbackDisplay>;
|
||||
|
||||
const Template: ComponentStory<typeof NotificationFeedbackDisplay> = (args) => <NotificationFeedbackDisplay {...args} />;
|
||||
|
||||
export const Common = Template.bind({});
|
||||
Common.args = {};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user