started migrating components to shadcn. also experimented with loading icon

This commit is contained in:
Jonathan Summers-Muir committed 2023-07-29 21:21:48 +08:00
1 parent 08ed506156
commit a08084c839
16 files changed
+1399 -340

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Do not edit directly
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
* Generated on Fri, 28 Jul 2023 11:07:13 GMT
*/
:root {
+6 -4
View File
@@ -1,13 +1,15 @@
/**
* Do not edit directly
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
* Generated on Fri, 28 Jul 2023 11:07:13 GMT
*/
.dark {
--brand-default: 152.9deg 60% 52.9%;
--brand-400: 153.6deg 41% 12%;
--brand-300: 152.7deg 48.5% 26.7%;
--brand-200: 148.5deg 43.5% 82%;
--brand-600: 152.9deg 60% 50%;
--brand-500: 153.5deg 61.8% 21.6%;
--brand-400: 153.3deg 65.2% 13.5%;
--brand-300: 153.8deg 69.6% 9%;
--brand-200: 152.5deg 75% 6.3%;
--warning-default: 38.9deg 100% 57.1%;
--warning-600: 38.9deg 89.8% 49.8%;
--warning-500: 34.8deg 90.9% 21.6%;
+7 -5
View File
@@ -1,13 +1,15 @@
/**
* Do not edit directly
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
* Generated on Fri, 28 Jul 2023 11:07:13 GMT
*/
.light {
--brand-primary: 152.9deg 60% 52.9%;
--brand-400: 153.7deg 68.7% 83.7%;
--brand-300: 153deg 46% 65.9%;
--brand-200: 148.5deg 43.5% 82%;
--brand-default: 152.9deg 60% 52.9%;
--brand-600: 153.2deg 49.7% 33.5%;
--brand-500: 148.7deg 42.7% 69.2%;
--brand-400: 149.1deg 59.3% 88.4%;
--brand-300: 148.6deg 77.8% 94.7%;
--brand-200: 156deg 71.4% 98.6%;
--warning-default: 38.9deg 100% 57.1%;
--warning-600: 30.2deg 100% 33.9%;
--warning-500: 36.3deg 85.7% 67.1%;
+11 -3
View File
@@ -529,15 +529,23 @@ module.exports = {
},
"brand-200": {
cssVariable: "var(--brand-200)",
value: "hsl(148.5, 43.5%, 82%)"
value: "hsl(152.5, 75%, 6.3%)"
},
"brand-300": {
cssVariable: "var(--brand-300)",
value: "hsl(152.70000000000005, 48.5%, 26.7%)"
value: "hsl(153.79999999999995, 69.6%, 9%)"
},
"brand-400": {
cssVariable: "var(--brand-400)",
value: "hsl(153.60000000000002, 41%, 12%)"
value: "hsl(153.29999999999995, 65.2%, 13.5%)"
},
"brand-500": {
cssVariable: "var(--brand-500)",
value: "hsl(153.5, 61.8%, 21.6%)"
},
"brand-600": {
cssVariable: "var(--brand-600)",
value: "hsl(152.89999999999998, 60%, 50%)"
},
"brand-DEFAULT": {
cssVariable: "var(--brand-default)",
+2
View File
@@ -119,6 +119,8 @@ export {
AlertDescription as AlertDescription_Shadcn_,
} from './src/components/shadcn/ui/alert'
export { Input as Input_Shadcn_ } from './src/components/shadcn/ui/input'
export {
useFormField as useFormField_Shadcn_,
Form as Form_Shadcn_,
@@ -1,6 +1,6 @@
import * as React from 'react'
import { cn } from 'src/lib/utils'
import { cn } from '@ui/lib/utils'
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -12,7 +12,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
...customClasses,
className
)}
@@ -41,6 +41,8 @@ export default {
"bg-brand-200",
"bg-brand-300",
"bg-brand-400",
"bg-brand-500",
"bg-brand-600",
"bg-brand"
],
"palletes": [
+29
View File
@@ -0,0 +1,29 @@
// make a function that returns a component
import { useState, useEffect } from 'react'
const AiIconAnimation = () => {
const [step, setStep] = useState(1)
useEffect(() => {
const interval = setInterval(() => {
setStep((step) => {
return (step % 5) + 1
})
}, 2000)
return () => clearInterval(interval)
}, [])
return (
<div className="ai-icon__container animate--[spin_5s_ease-in-out_infinite] spin-ai-icon-container">
<div className="ai-icon__grid">
<div className={`ai-icon__grid__square ai-icon__grid__square--step-${step}`}></div>
<div className={`ai-icon__grid__square ai-icon__grid__square--step-${step}`}></div>
<div className={`ai-icon__grid__square ai-icon__grid__square--step-${step}`}></div>
<div className={`ai-icon__grid__square ai-icon__grid__square--step-${step}`}></div>
</div>
</div>
)
}
export { AiIconAnimation }
@@ -4,6 +4,7 @@ import { editor } from 'monaco-editor'
import { MutableRefObject, useRef, useState } from 'react'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
import { IStandaloneCodeEditor } from './SQLEditor.types'
import { cn } from 'ui'
export type MonacoEditorProps = {
id: string
@@ -11,6 +12,7 @@ export type MonacoEditorProps = {
isExecuting: boolean
autoFocus?: boolean
executeQuery: () => void
className?: string
}
const MonacoEditor = ({
@@ -18,6 +20,7 @@ const MonacoEditor = ({
editorRef,
isExecuting,
autoFocus = true,
className,
executeQuery,
}: MonacoEditorProps) => {
const snap = useSqlEditorStateSnapshot({ sync: true })
@@ -73,7 +76,7 @@ const MonacoEditor = ({
return (
<>
<Editor
className="monaco-editor"
className={cn(className, 'monaco-editor')}
theme={'supabase'}
onMount={handleEditorOnMount}
onChange={handleEditorChange}
@@ -45,6 +45,8 @@ import {
IconSettings,
IconX,
Input,
Input_Shadcn_,
cn,
} from 'ui'
import AISettingsModal from './AISettingsModal'
import {
@@ -55,6 +57,7 @@ import {
import { IStandaloneCodeEditor, IStandaloneDiffEditor } from './SQLEditor.types'
import { createSqlSnippetSkeleton } from './SQLEditor.utils'
import UtilityPanel from './UtilityPanel/UtilityPanel'
import { AiIconAnimation } from 'components/animations/ai-icon'
// Load the monaco editor client-side only (does not behave well server-side)
const MonacoEditor = dynamic(() => import('./MonacoEditor'), { ssr: false })
@@ -136,6 +139,8 @@ const SQLEditor = () => {
const [sqlDiff, setSqlDiff] = useState<ContentDiff>()
const inputRef = useRef<HTMLInputElement>(null)
const [aiOpen, setAiOpen] = useState(false)
const [isAISettingsOpen, setIsAISettingsOpen] = useState(false)
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)
@@ -417,248 +422,333 @@ const SQLEditor = () => {
executeQuery(true)
}}
/>
<div className="flex h-full flex-col">
<motion.div
key="ask-ai-input-container"
layoutId="ask-ai-input-container"
variants={{
visible: {
borderRadius: 0,
},
}}
initial="visible"
animate="visible"
className="w-full flex justify-center z-10 mt-0.5 bg-brand-400"
>
<AnimatePresence initial={false} exitBeforeEnter>
{!isEditSqlLoading ? (
<motion.div
key="ask-ai-input"
className="w-full"
variants={{
visible: {
opacity: 1,
y: 0,
},
hidden: {
opacity: 0,
y: -25,
},
}}
initial="hidden"
animate="visible"
exit="hidden"
transition={{
duration: 0.1,
}}
>
<Input
autoFocus
size="xlarge"
value={aiInput}
onChange={(e) => setAiInput(e.currentTarget.value)}
disabled={isDiffOpen}
inputRef={inputRef}
icon={
<div className="h-full flex flex-row gap-3 items-center">
<motion.div layoutId="ask-ai-input-icon" className="ml-1">
<AiIcon className="w-4 h-4" />
</motion.div>
<div className="flex h-full flex-col relative">
{aiOpen && (
<motion.div
key="ask-ai-input-container"
layoutId="ask-ai-input-container"
variants={{
visible: {
borderRadius: 0,
},
}}
initial="visible"
animate="visible"
className="w-full flex justify-center z-10 h-[60px] bg-brand-200 border-b border-brand-400 px-5"
>
<div
className={cn(
'w-full !border-brand-900 border-none !shadow-none placeholder:text-scale-900',
'flex items-center gap-3'
)}
>
{/* <div className="group h-12 w-12 relative group"> */}
{/* <div className="absolute duration-2000 h-12 w-12 skew--y-[24deg]">
<div
className={cn(
'transition-all',
'absolute top-3 left-3 h-4 w-4 border border-brand',
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-75'
)}
></div>
<div
className={cn(
'transition-all',
'absolute top-3 left-5 h-4 w-4 border border-brand',
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-200'
)}
></div>
<div
className={cn(
'transition-all',
'absolute top-5 left-3 h-4 w-4 border border-brand',
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-400'
)}
></div>
<div
className={cn(
'transition-all',
'absolute top-5 left-5 h-4 w-4 border border-brand',
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-600'
)}
></div>
</div> */}
{debugSolution && (
<div className="h-full w-full flex flex-row items-center overflow-y-hidden mr-[16.5rem] text-sm text-white">
{debugSolution}
</div>
)}
</div>
}
inputClassName="w-full !border-brand-900 border-none bg-transparent !shadow-none py-4 focus:!ring-0 placeholder:text-scale-900 pr-20"
iconContainerClassName="transition text-scale-800 text-brand-900"
placeholder={!debugSolution ? 'Ask Supabase AI to modify your query' : ''}
className="w-full"
actions={
<div className="flex flex-row items-center gap-2 space-x-1 mr-6">
{isDiffOpen ? (
<>
<div className="flex items-center">
<Button
className="rounded-r-none"
type="primary"
size="tiny"
icon={
!isAcceptDiffLoading ? (
<IconCheck />
) : (
<IconLoader className="animate-spin" size={14} />
)
}
iconRight={
<div className="opacity-30">
<IconCornerDownLeft size={12} strokeWidth={1.5} />
</div>
}
onClick={acceptAiHandler}
>
{getDiffTypeButtonLabel(selectedDiffType)}
</Button>
<Dropdown
align="end"
side="bottom"
overlay={Object.values(DiffType)
.filter((diffType) => diffType !== selectedDiffType)
.map((diffType) => (
<Dropdown.Item
key={diffType}
onClick={() => {
setSelectedDiffType(diffType)
switch (diffType) {
case DiffType.Modification:
return compareAsModification()
case DiffType.Addition:
return compareAsAddition()
case DiffType.NewSnippet:
return compareAsNewSnippet()
default:
throw new Error(`Unknown diff type '${diffType}'`)
}
}}
>
{getDiffTypeDropdownLabel(diffType)}
</Dropdown.Item>
))}
>
<Button
type="primary"
className="rounded-l-none border-l-0 px-[4px] py-[5px]"
icon={<IconChevronDown />}
/>
</Dropdown>
</div>
<Button
type="alternative"
size="tiny"
icon={<IconX />}
iconRight={<span className="opacity-30">ESC</span>}
onClick={discardAiHandler}
>
Discard
</Button>
</>
) : (
<>
<IconCornerDownLeft size={16} strokeWidth={1.5} />
<IconSettings
className="cursor-pointer"
onClick={() => setIsAISettingsOpen(true)}
/>
</>
)}
</div>
}
onKeyPress={async (e) => {
if (e.key === 'Enter') {
try {
const prompt = e.currentTarget.value
if (!prompt) {
return
}
const sql = editorRef.current?.getValue()
if (!sql) {
return
}
const { sql: modifiedSql } = await editSql({
prompt,
sql: sql.replace(sqlAiDisclaimerComment, '').trim(),
entityDefinitions,
})
const formattedSql =
sqlAiDisclaimerComment +
'\n\n' +
format(modifiedSql, {
language: 'postgresql',
keywordCase: 'lower',
})
// TODO: show error
if (formattedSql.trim() === sql.trim()) {
ui.setNotification({
category: 'error',
message: 'Unable to edit SQL. Try adding more details to your prompt.',
})
return
}
setSqlDiff({
original: sql,
modified: formattedSql,
})
} catch (error: unknown) {
if (isError(error)) {
ui.setNotification({
category: 'error',
message: error.message,
})
}
}
}
}}
/>
</motion.div>
) : (
<motion.div
key="ask-ai-loading"
className="p-4 flex flex-row gap-2 items-center w-full"
variants={{
visible: {
opacity: 1,
y: 0,
},
hidden: {
opacity: 0,
y: 25,
},
}}
transition={{
duration: 0.2,
}}
initial="hidden"
animate="visible"
exit="hidden"
>
<motion.div
className="text-brand-900"
animate={{
scale: [0.9, 1.1, 0.9],
transition: {
ease: 'linear',
duration: 2,
repeat: Infinity,
},
}}
>
<AiIcon className="w-4 h-4" />
{/* <div className="absolute top-0.5 right-0.5 duration-2000 h-12 w-12 skew-y-[24deg] hover:top-0 hover:right-0">
<div
className={cn(
'transition-all',
'absolute top-3 left-3 h-4 w-4 border border-brand',
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-75'
)}
></div>
<div
className={cn(
'transition-all',
'absolute top-3 left-5 h-4 w-4 border border-brand',
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-200'
)}
></div>
<div
className={cn(
'transition-all',
'absolute top-5 left-3 h-4 w-4 border border-brand',
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-400'
)}
></div>
<div
className={cn(
'transition-all',
'absolute top-5 left-5 h-4 w-4 border border-brand',
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-600'
)}
></div>
</div> */}
{/* </div> */}
{/* <div className="flex flex-row gap-3 items-center">
<motion.div layoutId="ask-ai-input-icon">
<AiIcon className="w-4 h-4 text-brand" />
</motion.div>
<motion.span
animate={{
opacity: ['0.5', '0.75', '0.5'],
transition: {
ease: 'linear',
duration: 2,
repeat: Infinity,
},
}}
>
Thinking...
</motion.span>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div> */}
<AiIconAnimation />
<AnimatePresence initial={false} exitBeforeEnter>
{debugSolution && (
<div className="h-full w-full flex flex-row items-center overflow-y-hidden text-sm text-white">
{debugSolution}
</div>
)}
{!isEditSqlLoading ? (
<motion.div
key="ask-ai-input"
className="w-full h-full relative flex items-center"
variants={{
visible: {
opacity: 1,
y: 0,
},
hidden: {
opacity: 0,
y: -25,
},
}}
initial="hidden"
animate="visible"
exit="hidden"
transition={{
duration: 0.1,
}}
>
<Input_Shadcn_
autoFocus
value={aiInput}
onChange={(e) => setAiInput(e.currentTarget.value)}
disabled={isDiffOpen}
ref={inputRef}
className={cn(
'!p-0 bg-transparent border-transparent text-sm text-brand placeholder:text-brand-600 focus:!ring-0',
'focus-visible:ring-0 focus-visible:ring-offset-0',
'appearance-none outline-none'
// "after:content-['_↗']",
// 'after:relative after:w-10 after:h-10',
// 'after:placeholder:content-["hello"]'
)}
placeholder={!debugSolution ? 'Ask Supabase AI to modify your query' : ''}
onKeyPress={async (e) => {
if (e.key === 'Enter') {
console.log('entering')
try {
const prompt = e.currentTarget.value
if (!prompt) {
return
}
const sql = editorRef.current?.getValue()
if (!sql) {
return
}
const { sql: modifiedSql } = await editSql({
prompt,
sql: sql.replace(sqlAiDisclaimerComment, '').trim(),
entityDefinitions,
})
const formattedSql =
sqlAiDisclaimerComment +
'\n\n' +
format(modifiedSql, {
language: 'postgresql',
keywordCase: 'lower',
})
// TODO: show error
if (formattedSql.trim() === sql.trim()) {
ui.setNotification({
category: 'error',
message:
'Unable to edit SQL. Try adding more details to your prompt.',
})
return
}
setSqlDiff({
original: sql,
modified: formattedSql,
})
} catch (error: unknown) {
if (
error &&
typeof error === 'object' &&
'message' in error &&
typeof error.message === 'string'
) {
ui.setNotification({
category: 'error',
message: error.message,
})
}
}
}
}}
/>
{/* <div className="opacity-30">
<IconCornerDownLeft size={12} strokeWidth={1.5} />
</div> */}
</motion.div>
) : (
<motion.div
key="ask-ai-loading"
className="p-0 flex flex-row gap-2 items-center w-full"
variants={{
visible: {
opacity: 1,
y: 0,
},
hidden: {
opacity: 0,
y: 25,
},
}}
transition={{
duration: 0.2,
}}
initial="hidden"
animate="visible"
exit="hidden"
>
<motion.span
className="text-sm text-brand px-3"
animate={{
opacity: ['0.5', '0.75', '0.5'],
transition: {
ease: 'linear',
duration: 0.33,
repeat: Infinity,
},
}}
>
Thinking...
</motion.span>
</motion.div>
)}
</AnimatePresence>
<div className="flex flex-row items-center gap-2">
{isDiffOpen ? (
<>
<div className="flex items-center">
<Button
className="rounded-r-none"
type="primary"
size="tiny"
icon={
!isAcceptDiffLoading ? (
<IconCheck />
) : (
<IconLoader className="animate-spin" size={14} />
)
}
iconRight={
<div className="opacity-30">
<IconCornerDownLeft size={12} strokeWidth={1.5} />
</div>
}
onClick={acceptAiHandler}
>
{getDiffTypeButtonLabel(selectedDiffType)}
</Button>
<Dropdown
align="end"
side="bottom"
overlay={Object.values(DiffType)
.filter((diffType) => diffType !== selectedDiffType)
.map((diffType) => (
<Dropdown.Item
key={diffType}
onClick={() => {
setSelectedDiffType(diffType)
switch (diffType) {
case DiffType.Modification:
return compareAsModification()
case DiffType.Addition:
return compareAsAddition()
case DiffType.NewSnippet:
return compareAsNewSnippet()
default:
throw new Error(`Unknown diff type '${diffType}'`)
}
}}
>
{getDiffTypeDropdownLabel(diffType)}
</Dropdown.Item>
))}
>
<Button
type="primary"
className="rounded-l-none border-l-0 px-[4px] py-[5px]"
icon={<IconChevronDown />}
/>
</Dropdown>
</div>
<Button
type="alternative"
size="tiny"
icon={<IconX />}
iconRight={<span className="opacity-30">ESC</span>}
onClick={discardAiHandler}
>
Discard
</Button>
</>
) : (
<>
<div
className={cn('transition text-brand', !aiInput ? 'opacity-0' : 'opacity-30')}
>
<IconCornerDownLeft size={16} strokeWidth={1.5} />
</div>
<button
className="text-brand hover:text-brand-600"
onClick={() => setAiOpen(false)}
>
<IconX size={21} />
</button>
<button
onClick={() => setIsAISettingsOpen(true)}
className="text-lighter hover:text transition"
>
<IconSettings className="cursor-pointer" />
</button>
</>
)}
</div>
</div>
</motion.div>
)}
<Split
style={{ height: '100%' }}
direction="vertical"
@@ -672,7 +762,33 @@ const SQLEditor = () => {
collapsed={isUtilityPanelCollapsed ? 1 : undefined}
onDragEnd={onDragEnd}
>
<div className="dark:border-dark flex-grow overflow-y-auto border-b">
<div className="flex-grow overflow-y-auto border-b">
{!aiOpen && (
<button
onClick={() => setAiOpen(!aiOpen)}
className={cn(
'group',
'absolute z-10',
'rounded-lg',
'right-5 top-1.5',
'w-10 h-10 bg-brand-400 border border-brand-600 shadow-sm',
'transition-all duration-200',
'ease-out hover:w-[155px]',
'flex items-center justify-center group-hover:justify-between gap-3'
)}
>
<span
className={cn(
'hidden text-brand w-0 ',
'group-hover:w-auto group-hover:inline-block truncate'
)}
>
Supabase AI
</span>
<AiIcon className="text-brand w-4 h-4" />
</button>
)}
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">Loading...</div>
) : (
@@ -0,0 +1,284 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useParams, useTelemetryProps } from 'common'
import {
SQL_TEMPLATES,
sqlAiDisclaimerComment,
} from 'components/interfaces/SQLEditor/SQLEditor.constants'
import { useSqlGenerateMutation } from 'data/ai/sql-generate-mutation'
import { SqlSnippet } from 'data/content/sql-snippets-query'
import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query'
import { isError } from 'data/utils/error-check'
import { motion } from 'framer-motion'
import {
useCheckPermissions,
useLocalStorageQuery,
useSelectedOrganization,
useSelectedProject,
useStore,
} from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import Telemetry from 'lib/telemetry'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { format } from 'sql-formatter'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
import { AiIcon, IconCornerDownLeft, IconSettings, Input } from 'ui'
import AISettingsModal from '../AISettingsModal'
import { createSqlSnippetSkeleton } from '../SQLEditor.utils'
import SQLCard from './SQLCard'
const SQLQuickstarts = observer(() => {
const { ui } = useStore()
const { ref } = useParams()
const router = useRouter()
const { profile } = useProfile()
const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' })
const { mutateAsync: generateSql, isLoading: isSqlGenerateLoading } = useSqlGenerateMutation()
const [isAISettingsOpen, setIsAISettingsOpen] = useState(false)
const selectedOrganization = useSelectedOrganization()
const selectedProject = useSelectedProject()
const isOptedInToAI =
selectedOrganization?.opt_in_tags?.includes('AI_SQL_GENERATOR_OPT_IN') ?? false
const [isOptedInToAISchema] = useLocalStorageQuery('supabase_sql-editor-ai-schema', false)
const includeSchemaMetadata = (isOptedInToAI || !IS_PLATFORM) && isOptedInToAISchema
const { data } = useEntityDefinitionsQuery(
{
projectRef: selectedProject?.ref,
connectionString: selectedProject?.connectionString,
},
{ enabled: includeSchemaMetadata }
)
const entityDefinitions = includeSchemaMetadata ? data?.map((def) => def.sql.trim()) : undefined
const telemetryProps = useTelemetryProps()
const snap = useSqlEditorStateSnapshot()
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
resource: { type: 'sql', owner_id: profile?.id },
subject: { id: profile?.id },
})
// [Joshen TODO] Removed optimistic query creation logic for now, need to figure out
// how to do that after using ids as part of the URL
const handleNewQuery = async (sql: string, name: string) => {
if (!ref) return console.error('Project ref is required')
if (!canCreateSQLSnippet) {
return ui.setNotification({
category: 'info',
message: 'Your queries will not be saved as you do not have sufficient permissions',
})
}
try {
const snippet = createSqlSnippetSkeleton({ name, sql, owner_id: profile?.id })
const data = { ...snippet, id: uuidv4() }
snap.addSnippet(data as SqlSnippet, ref, true)
router.push(`/project/${ref}/sql/${data.id}`)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to create new query: ${error.message}`,
})
}
}
return (
<>
<AISettingsModal visible={isAISettingsOpen} onCancel={() => setIsAISettingsOpen(false)} />
<div className="block h-full space-y-8 overflow-y-auto p-6">
<div className="mt-32 mb-32 flex flex-col items-center">
<motion.h1
className="text-scale-1200 mb-8 text-3xl"
variants={{
visible: {
y: 0,
opacity: 1,
},
hidden: {
y: 10,
opacity: 0,
},
}}
initial="hidden"
animate="visible"
>
What do you want to build?
</motion.h1>
<div className="w-full flex justify-center">
{!isSqlGenerateLoading ? (
<motion.div
key="ask-ai-input-container"
layoutId="ask-ai-input-container"
className="w-full max-w-2xl border border-brand-900"
variants={{
visible: {
y: 0,
opacity: 1,
borderRadius: 6,
},
hidden: {
y: -50,
opacity: 0,
borderRadius: 6,
},
}}
initial="hidden"
animate="visible"
>
<Input
size="xlarge"
inputRef={(inputElement: HTMLInputElement) => {
setTimeout(() => {
inputElement?.focus()
}, 200)
}}
icon={
<motion.div
key="ask-ai-input-icon"
layoutId="ask-ai-input-icon"
className="ml-1"
variants={{
visible: {
scale: 1,
},
}}
initial="visible"
animate="visible"
>
<AiIcon className="w-4 h-4" />
</motion.div>
}
inputClassName="w-full border-none py-4 focus:!ring-0 pr-20"
iconContainerClassName="transition text-scale-800 text-brand-900"
placeholder="Ask Supabase AI to build a query"
className="w-full"
onKeyPress={async (e) => {
if (e.key === 'Enter') {
try {
const value = e.currentTarget.value
if (!value) {
return
}
const { title, sql } = await generateSql({
prompt: e.currentTarget.value,
entityDefinitions,
})
const formattedSql =
sqlAiDisclaimerComment +
'\n\n' +
format(sql, {
language: 'postgresql',
keywordCase: 'lower',
})
await handleNewQuery(formattedSql, title)
} catch (error: unknown) {
if (isError(error)) {
ui.setNotification({
category: 'error',
message: error.message,
})
}
}
}
}}
actions={
<div className="flex items-center space-x-1 mr-4 gap-2">
<IconCornerDownLeft size={16} strokeWidth={1.5} />
<IconSettings
className="cursor-pointer"
onClick={() => setIsAISettingsOpen(true)}
/>
</div>
}
/>
</motion.div>
) : (
<motion.div
key="ask-ai-loading"
layoutId="ask-ai-input-container"
className="p-5 border border-brand-900 text-brand-900"
variants={{
visible: {
borderRadius: 50,
},
}}
transition={{
type: 'spring',
mass: 0.1,
stiffness: 200,
damping: 30,
}}
initial="visible"
animate="visible"
>
<motion.div
key="ask-ai-loading-icon"
layoutId="ask-ai-input-icon"
animate={{
scale: [0.9, 1.1, 0.9],
transition: {
delay: 0.2,
ease: 'linear',
duration: 2,
repeat: Infinity,
},
}}
>
<AiIcon className="w-4 h-4" />
</motion.div>
</motion.div>
)}
</div>
</div>
<div className="mb-8">
<div className="mb-4">
<h1 className="text-scale-1200 mb-3 text-xl">Quick start</h1>
<p className="text-scale-1100 text-sm">
While we're in beta, we want to offer a quick way to explore Supabase. While we build
importers, check out these simple starters.
</p>
<p className="text-scale-1100 text-sm">
Click on any script to fill the query box, modify the script, then click
<span className="text-code">Run</span>.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{quickStart.map((x) => (
<SQLCard
key={x.title}
title={x.title}
description={x.description}
sql={x.sql}
onClick={(sql, title) => {
handleNewQuery(sql, title)
Telemetry.sendEvent(
{
category: 'quickstart',
action: 'quickstart_clicked',
label: x.title,
},
telemetryProps,
router
)
}}
/>
))}
</div>
</div>
</div>
</>
)
})
export default SQLQuickstarts
@@ -273,41 +273,6 @@ const SQLTemplates = observer(() => {
))}
</div>
</div>
<div className="mb-8">
<div className="mb-4">
<h1 className="text-scale-1200 mb-3 text-xl">Quick start</h1>
<p className="text-scale-1100 text-sm">
While we're in beta, we want to offer a quick way to explore Supabase. While we build
importers, check out these simple starters.
</p>
<p className="text-scale-1100 text-sm">
Click on any script to fill the query box, modify the script, then click
<span className="text-code">Run</span>.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{quickStart.map((x) => (
<SQLCard
key={x.title}
title={x.title}
description={x.description}
sql={x.sql}
onClick={(sql, title) => {
handleNewQuery(sql, title)
Telemetry.sendEvent(
{
category: 'quickstart',
action: 'quickstart_clicked',
label: x.title,
},
telemetryProps,
router
)
}}
/>
))}
</div>
</div>
</div>
</>
)
@@ -1,3 +1,4 @@
import SQLTemplates from './SQLTemplates/SQLTemplates'
import SQLQuickstarts from './SQLTemplates/SQLQuickstarts'
export { SQLTemplates }
export { SQLTemplates, SQLQuickstarts }
@@ -14,7 +14,7 @@ import { useCheckPermissions, useStore } from 'hooks'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useSnippets, useSqlEditorStateSnapshot } from 'state/sql-editor'
import { IconSearch, IconX, Input, Menu, cn } from 'ui'
import { Button, IconSearch, IconX, Input, Menu, cn } from 'ui'
import QueryItem from './QueryItem'
const SideBarContent = observer(() => {
@@ -98,20 +98,31 @@ const SideBarContent = observer(() => {
) : isSuccess ? (
<div className="space-y-6">
<div className="px-3 flex flex-col gap-2">
<ProductMenuItem
name="Build a query"
isActive={id === undefined}
url={`/project/${ref}/sql`}
/>
<ProductMenuItem
name="New empty query"
isActive={false}
<Button
// isActive={false}
type="default"
className="mx-3"
onClick={() => {
handleNewQuery()
}}
/>
icon={<IconSearch size="tiny" />}
>
New query
</Button>
</div>
<div className="space-y-6 px-3">
<div>
<ProductMenuItem
name="Templates"
isActive={router.asPath === `/project/${ref}/sql`}
url={`/project/${ref}/sql`}
/>
<ProductMenuItem
name="Quickstarts"
isActive={router.asPath === `/project/${ref}/sql/quickstarts`}
url={`/project/${ref}/sql/quickstarts`}
/>
</div>
{favorites.length >= 1 && (
<div className="editor-product-menu">
<div className="flex flex-row justify-between">
@@ -158,52 +169,58 @@ const SideBarContent = observer(() => {
</div>
</div>
)}
{queries.length >= 1 && (
<div className="editor-product-menu">
<div className="flex flex-row justify-between">
<Menu.Group title="SQL snippets" />
<div className="editor-product-menu">
<div className="flex flex-row justify-between">
<Menu.Group title="SQL snippets" />
<button
className="flex items-center w-4 h-4 cursor-pointer mr-3"
onClick={() => {
setIsSnippetsFilterOpen((state) => !state)
}}
>
<IconSearch
className={cn(
'w-4',
'h-4',
'cursor-pointer',
isSnippetsFilterOpen ? 'text-scale-1200' : 'text-scale-900'
)}
onClick={() => {
setIsSnippetsFilterOpen((state) => !state)
}}
className={cn(isSnippetsFilterOpen ? 'text-scale-1200' : 'text-scale-900')}
/>
</button>
</div>
{isSnippetsFilterOpen && (
<div className="pl-3 mb-2 mr-3">
<Input
autoFocus
size="tiny"
icon={<IconSearch size="tiny" />}
placeholder="Filter"
disabled={isLoading}
onChange={(e) => setSnippetsFilterString(e.target.value)}
value={snippetsFilterString}
actions={
snippetsFilterString && (
<IconX
size={'tiny'}
className="mr-2 cursor-pointer"
onClick={() => setSnippetsFilterString('')}
/>
)
}
/>
</div>
{isSnippetsFilterOpen && (
<div className="pl-3 mb-2">
<Input
autoFocus
size="tiny"
icon={<IconSearch size="tiny" />}
placeholder="Filter"
disabled={isLoading}
onChange={(e) => setSnippetsFilterString(e.target.value)}
value={snippetsFilterString}
actions={
snippetsFilterString && (
<IconX
size={'tiny'}
className="mr-2 cursor-pointer"
onClick={() => setSnippetsFilterString('')}
/>
)
}
/>
</div>
)}
)}
{snippetsTabs.length > 0 ? (
<div className="space-y-1 pb-8">
{snippetsTabs.map((tabInfo) => {
const { id } = tabInfo || {}
return <QueryItem key={id} tabInfo={tabInfo} />
})}
</div>
</div>
)}
) : (
<div className="text text-sm h-32 border border-dashed flex flex-col gap-3 items-center justify-center px-3 mx-3 rounded">
<span className="text-lighter">No queries found</span>
<Button type="default">New Query</Button>
</div>
)}
</div>
{snippetsFilterString.length > 0 &&
favouriteTabs.length === 0 &&
snippetsTabs.length === 0 && (
@@ -0,0 +1,12 @@
import { observer } from 'mobx-react-lite'
import { NextPageWithLayout } from 'types'
import { SQLEditorLayout } from 'components/layouts'
import { SQLQuickstarts } from 'components/interfaces/SQLEditor'
const SqlEditorWelcome: NextPageWithLayout = () => {
return <SQLQuickstarts />
}
SqlEditorWelcome.getLayout = (page) => <SQLEditorLayout title="Quickstarts">{page}</SQLEditorLayout>
export default observer(SqlEditorWelcome)
+615
View File
@@ -46,3 +46,618 @@ span.th.tr.td.textarea {
background: var(--vscode-quickInput-background);
}
}
/*
* STEP ONE
*/
@keyframes step1_square1 {
0% {
transform: rotate(45deg);
top: 10px;
left: 10px;
width: 8px;
height: 8px;
border-radius: 100%;
}
100% {
transform: rotate(0deg);
top: 0px;
left: 0px;
width: 16px;
height: 16px;
}
}
@keyframes step1_square2 {
0% {
transform: rotate(45deg);
top: 10px;
left: 10px;
width: 8px;
height: 8px;
border-radius: 100%;
}
100% {
transform: rotate(0deg);
top: 0px;
left: 8px;
width: 16px;
height: 16px;
}
}
@keyframes step1_square3 {
0% {
transform: rotate(45deg);
top: 10px;
left: 10px;
width: 8px;
height: 8px;
border-radius: 100%;
}
100% {
transform: rotate(0deg);
top: 8px;
left: 0px;
width: 16px;
height: 16px;
}
}
@keyframes step1_square4 {
0% {
transform: rotate(45deg);
top: 10px;
left: 10px;
width: 8px;
height: 8px;
border-radius: 100%;
}
100% {
transform: rotate(0deg);
top: 8px;
left: 8px;
width: 16px;
height: 16px;
}
}
/*
* STEP TWO
*/
@keyframes step2_square1 {
0% {
transform: rotate(0deg);
top: 0px;
left: 0px;
width: 16px;
height: 16px;
}
100% {
transform: rotate(45deg);
top: 0px;
left: 0px;
width: 8px;
height: 8px;
}
}
@keyframes step2_square2 {
0% {
transform: rotate(0deg);
top: 0px;
left: 8px;
width: 16px;
height: 16px;
}
100% {
transform: rotate(45deg);
top: 0px;
left: 16px;
width: 8px;
height: 8px;
}
}
@keyframes step2_square3 {
0% {
transform: rotate(0deg);
top: 8px;
left: 0px;
width: 16px;
height: 16px;
}
100% {
transform: rotate(45deg);
top: 16px;
left: 0px;
width: 8px;
height: 8px;
}
}
@keyframes step2_square4 {
0% {
transform: rotate(0deg);
top: 8px;
left: 8px;
width: 16px;
height: 16px;
}
100% {
transform: rotate(45deg);
top: 16px;
left: 16px;
width: 8px;
height: 8px;
}
}
/*
* STEP THREE
*/
@keyframes step3_square1 {
0% {
transform: rotate(45deg);
top: 0px;
left: 0px;
width: 8px;
height: 8px;
}
100% {
transform: rotate(0deg);
top: 0px;
left: 0px;
width: 6px;
height: 6px;
border-radius: 100%;
}
}
@keyframes step3_square2 {
0% {
transform: rotate(45deg);
top: 0px;
left: 16px;
width: 8px;
height: 8px;
}
100% {
transform: rotate(0deg);
top: 0px;
left: 18px;
width: 6px;
height: 6px;
border-radius: 100%;
}
}
@keyframes step3_square3 {
0% {
transform: rotate(45deg);
top: 16px;
left: 0px;
width: 8px;
height: 8px;
}
100% {
transform: rotate(0deg);
top: 18px;
left: 0px;
width: 6px;
height: 6px;
border-radius: 100%;
}
}
@keyframes step3_square4 {
0% {
transform: rotate(45deg);
top: 16px;
left: 16px;
width: 8px;
height: 8px;
}
100% {
transform: rotate(0deg);
top: 18px;
left: 18px;
width: 6px;
height: 6px;
border-radius: 100%;
}
}
/*
* STEP FOUR
*/
@keyframes step4_square1 {
0% {
transform: rotate(0deg);
top: 0px;
left: 0px;
width: 6px;
height: 6px;
border-radius: 100%;
}
100% {
transform: rotate(0deg);
top: 0px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 100%;
}
}
@keyframes step4_square2 {
0% {
transform: rotate(0deg);
top: 0px;
left: 18px;
width: 6px;
height: 6px;
border-radius: 100%;
}
100% {
transform: rotate(0deg);
top: 8px;
left: 16px;
width: 8px;
height: 8px;
border-radius: 100%;
}
}
@keyframes step4_square3 {
0% {
transform: rotate(0deg);
top: 18px;
left: 0px;
width: 6px;
height: 6px;
border-radius: 100%;
}
100% {
transform: rotate(0deg);
top: 8px;
left: 0px;
width: 8px;
height: 8px;
border-radius: 100%;
}
}
@keyframes step4_square4 {
0% {
transform: rotate(0deg);
top: 18px;
left: 18px;
width: 6px;
height: 6px;
border-radius: 100%;
}
100% {
transform: rotate(0deg);
top: 16px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 100%;
}
}
// STEP FIVE
@keyframes step5_square1 {
0% {
transform: rotate(0deg);
top: 0px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 100%;
}
100% {
transform: rotate(45deg);
top: 8px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 100%;
}
}
@keyframes step5_square2 {
0% {
transform: rotate(0deg);
top: 8px;
left: 16px;
width: 8px;
height: 8px;
border-radius: 100%;
}
100% {
transform: rotate(45deg);
top: 8px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 100%;
}
}
@keyframes step5_square3 {
0% {
transform: rotate(0deg);
top: 8px;
left: 0px;
width: 8px;
height: 8px;
border-radius: 100%;
}
100% {
transform: rotate(45deg);
top: 8px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 100%;
}
}
@keyframes step5_square4 {
0% {
transform: rotate(0deg);
top: 16px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 100%;
}
100% {
transform: rotate(45deg);
top: 8px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 100%;
}
}
@keyframes spinAiIconContainer {
0% {
transform: rotate(0deg);
}
40% {
transform: rotate(-45deg);
}
// 40% {
// transform: rotate(-45deg);
// }
// 60% {
// transform: rotate(90deg); /* 360 degrees * 3 (3 full spins) */
// }
80% {
transform: rotate(180deg); /* 360 degrees * 3 (3 full spins) */
}
100% {
transform: rotate(180deg); /* 360 degrees * 3 (3 full spins) */
}
// 90% {
// transform: rotate(360deg); /* 360 degrees * 5 (5 full spins) */
// }
// 100% {
// transform: rotate(360deg); /* 360 degrees * 5 (5 full spins) */
// }
}
.spin-ai-icon-container {
// width: 100%;
// height: 100%;
// background-color: #f00; /* Replace this with your desired background color */
// border-radius: 50%;
// position: absolute;
// animation: spinAiIconContainer 10s infinite;
// animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
// animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
}
/*
* STEP SIX
*/
// @keyframes step6_square1 {
// 0% {
// transform: rotate(45deg);
// left: 0px;
// top: 0px;
// width: 12px;
// height: 12px;
// }
// 100% {
// transform: rotate(0deg);
// top: 8px;
// left: 8px;
// width: 8px;
// height: 8px;
// }
// }
// @keyframes step6_square2 {
// 0% {
// transform: rotate(45deg);
// top: 0px;
// left: 8px;
// width: 12px;
// height: 12px;
// }
// 100% {
// transform: rotate(0deg);
// top: 8px;
// left: 8px;
// width: 8px;
// height: 8px;
// }
// }
// @keyframes step6_square3 {
// 0% {
// transform: rotate(45deg);
// top: 8px;
// left: 0px;
// width: 12px;
// height: 12px;
// }
// 100% {
// transform: rotate(0deg);
// top: 8px;
// left: 8px;
// width: 8px;
// height: 8px;
// }
// }
// @keyframes step6_square4 {
// 0% {
// transform: rotate(45deg);
// top: 8px;
// left: 8px;
// width: 12px;
// height: 12px;
// }
// 100% {
// transform: rotate(0deg);
// top: 8px;
// left: 8px;
// width: 8px;
// height: 8px;
// }
// }
.ai-icon__container {
width: 24px;
height: 24px;
// background: red;
}
.ai-icon__grid {
position: relative;
width: 24px;
height: 24px;
// background: yellow;
}
.ai-icon__grid__square {
position: absolute;
border: 1px solid hsl(var(--brand-default));
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-1:nth-child(1) {
animation: step1_square1 700ms forwards;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
// animation-delay: 0;
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-1:nth-child(2) {
animation: step1_square2 700ms forwards;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
// animation-delay: 200ms;
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-1:nth-child(3) {
animation: step1_square3 700ms forwards;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
// animation-delay: 400ms;
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-1:nth-child(4) {
animation: step1_square4 700ms forwards;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
// animation-delay: 600ms;
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-2:nth-child(1) {
animation-delay: 200;
animation: step2_square1 0.7s forwards;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-2:nth-child(2) {
animation-delay: 400ms;
animation: step2_square2 0.7s forwards;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-2:nth-child(3) {
animation-delay: 600ms;
animation: step2_square3 0.7s forwards;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-2:nth-child(4) {
animation-delay: 800ms;
animation: step2_square4 0.7s forwards;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-3:nth-child(1) {
animation: step3_square1 0.7s forwards;
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-3:nth-child(2) {
animation: step3_square2 0.7s forwards;
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-3:nth-child(3) {
animation: step3_square3 0.7s forwards;
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-3:nth-child(4) {
animation: step3_square4 0.7s forwards;
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-4:nth-child(1) {
animation: step4_square1 1s forwards;
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-4:nth-child(2) {
animation: step4_square2 1s forwards;
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-4:nth-child(3) {
animation: step4_square3 1s forwards;
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-4:nth-child(4) {
animation: step4_square4 1s forwards;
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-5:nth-child(1) {
animation: step5_square1 3s forwards;
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-5:nth-child(2) {
animation: step5_square2 3s forwards;
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-5:nth-child(3) {
animation: step5_square3 3s forwards;
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-5:nth-child(4) {
animation: step5_square4 3s forwards;
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}