Compare commits
4 Commits
87ce5ff1d9
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b64c633549 | ||
|
|
0625487af0 | ||
|
|
64610e997d | ||
|
|
2983669c58 |
@@ -9,8 +9,8 @@ import globals from "@/app.globals.json";
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams();
|
||||
const slug = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
|
||||
const { key } = resolveRoute(slug);
|
||||
const segments = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
|
||||
const { key } = resolveRoute(segments);
|
||||
const { root, content } = (schema as any)[key] ?? {};
|
||||
const data = { root, content, globals };
|
||||
return <Render config={appConfig as any} data={data} />;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { CLOUD_BASE, FRONTEND_API_KEY } from "@/lib/cloud";
|
||||
|
||||
// Proxy the editor's AI chat requests to Frontend Cloud, injecting the API key
|
||||
// server-side so it is never exposed to the client. The cloud response is
|
||||
// streamed straight back to the caller.
|
||||
export async function POST(req: NextRequest) {
|
||||
const upstream = await fetch(`${CLOUD_BASE}/api/chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": req.headers.get("content-type") ?? "application/json",
|
||||
"x-api-key": FRONTEND_API_KEY,
|
||||
},
|
||||
body: req.body,
|
||||
// Required by undici when forwarding a streaming request body.
|
||||
// @ts-expect-error - duplex is valid but missing from the lib types.
|
||||
duplex: "half",
|
||||
});
|
||||
|
||||
return new Response(upstream.body, {
|
||||
status: upstream.status,
|
||||
headers: {
|
||||
"content-type":
|
||||
upstream.headers.get("content-type") ?? "application/octet-stream",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { useParams } from "next/navigation";
|
||||
import { Editor, blocksPlugin, outlinePlugin } from "@reacteditor/core";
|
||||
import createTailwindCdnPlugin from "@reacteditor/plugin-tailwind-cdn";
|
||||
import { createShopifyPlugin } from "@reacteditor/plugin-shopify";
|
||||
import { aiPlugin } from "@reacteditor/plugin-ai";
|
||||
import { mediaPlugin } from "@reacteditor/plugin-media";
|
||||
import { mediaAdapter } from "@/lib/adapters/media-adapter";
|
||||
import { appConfig } from "@/editor.config";
|
||||
@@ -15,17 +14,13 @@ import globals from "@/app.globals.json";
|
||||
|
||||
export default function EditorPage() {
|
||||
const params = useParams();
|
||||
const slug = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
|
||||
const { key, path, params: routeParams } = resolveRoute(slug);
|
||||
const segments = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
|
||||
const { key, path, params: routeParams } = resolveRoute(segments);
|
||||
const { root, content } = (schema as any)[key] ?? {};
|
||||
const data = { root, content, globals };
|
||||
|
||||
const plugins = useMemo(
|
||||
() => [
|
||||
aiPlugin({
|
||||
// Proxied through our server route so the API key stays server-side.
|
||||
api: "/api/chat",
|
||||
}),
|
||||
blocksPlugin(),
|
||||
outlinePlugin(),
|
||||
mediaPlugin({
|
||||
@@ -59,7 +54,6 @@ export default function EditorPage() {
|
||||
data={data}
|
||||
route={{ key, path, params: routeParams }}
|
||||
plugins={plugins}
|
||||
ui={{ leftSideBarVisible: false }}
|
||||
onPublish={handlePublish}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useRouteHandle } from '@/lib/resolve-route';
|
||||
import { useRouteSegment } from '@/hooks/use-route-segment';
|
||||
import { ChevronDown, SlidersHorizontal } from 'lucide-react';
|
||||
import type { ShopifyCollection } from '@reacteditor/field-shopify';
|
||||
import {
|
||||
@@ -358,7 +358,7 @@ function buildProductFilters(active: ActiveFilters): ProductFilter[] {
|
||||
|
||||
export function CollectionView(props: CollectionProps) {
|
||||
const { collection: selected, showDescription, showCoverImage, customCoverImage, columns, limit, defaultSort } = props;
|
||||
const routeHandle = useRouteHandle();
|
||||
const routeHandle = useRouteSegment();
|
||||
const handle = selected?.handle ?? routeHandle ?? '';
|
||||
|
||||
const [sort, setSort] = useState<CollectionSortKey>(defaultSort);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useProduct, type Product } from '@/hooks/use-shopify-products';
|
||||
import { useRouteHandle } from '@/lib/resolve-route';
|
||||
import { useRouteSegment } from '@/hooks/use-route-segment';
|
||||
import { useShopifyCart } from '@/hooks/use-shopify-cart';
|
||||
import ProductDetailGallery from './product-detail-gallery';
|
||||
import ProductDetailInfo from './product-detail-info';
|
||||
@@ -44,7 +44,7 @@ interface ProductDetailProps {
|
||||
}
|
||||
|
||||
const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) => {
|
||||
const routeHandle = useRouteHandle();
|
||||
const routeHandle = useRouteSegment();
|
||||
const handle = handleProp || routeHandle || '';
|
||||
const { addItem, openCart } = useShopifyCart();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouteHandle } from "@/lib/resolve-route";
|
||||
import { useRouteSegment } from "@/hooks/use-route-segment";
|
||||
import type { ShopifyProduct } from "@reacteditor/field-shopify";
|
||||
import { useProduct } from "@/hooks/use-shopify-products";
|
||||
import { useShopifyCart } from "@/hooks/use-shopify-cart";
|
||||
@@ -14,7 +14,7 @@ export type ProductDetailsProps = {
|
||||
};
|
||||
|
||||
export function ProductDetailsView({ product: selected }: ProductDetailsProps) {
|
||||
const routeHandle = useRouteHandle();
|
||||
const routeHandle = useRouteSegment();
|
||||
const handle = selected?.handle ?? routeHandle ?? null;
|
||||
const { product, loading } = useProduct(handle);
|
||||
const cart = useShopifyCart();
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useProduct,
|
||||
useProductRecommendations,
|
||||
} from '@/hooks/use-shopify-products';
|
||||
import { useRouteHandle } from '@/lib/resolve-route';
|
||||
import { useRouteSegment } from '@/hooks/use-route-segment';
|
||||
import { ProductCard } from './product-card';
|
||||
|
||||
export type ProductRecommendationsProps = {
|
||||
@@ -20,7 +20,7 @@ export function ProductRecommendationsView({
|
||||
heading,
|
||||
limit,
|
||||
}: ProductRecommendationsProps) {
|
||||
const routeHandle = useRouteHandle();
|
||||
const routeHandle = useRouteSegment();
|
||||
const handle = selected?.handle ?? routeHandle ?? null;
|
||||
const { product } = useProduct(handle);
|
||||
const { recommendations, loading, error } = useProductRecommendations(
|
||||
|
||||
17
hooks/use-route-segment.ts
Normal file
17
hooks/use-route-segment.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Returns the last segment of the current catch-all slug route, e.g. the
|
||||
* `cool-shirt` in `/products/cool-shirt`. Components use this to derive the
|
||||
* resource they should load from the Next.js route segments directly.
|
||||
*/
|
||||
export function useRouteSegment(): string | undefined {
|
||||
const params = useParams();
|
||||
const segments = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
|
||||
// Editor routes are served under `/editor/*`; ignore that prefix so the
|
||||
// resolved segment matches the public route.
|
||||
const routeSegments = segments[0] === "editor" ? segments.slice(1) : segments;
|
||||
return routeSegments[routeSegments.length - 1];
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
const TEMPLATE_PATTERNS: { key: string; prefix: string; param: string }[] = [
|
||||
{ key: "/products/:handle", prefix: "/products/", param: "handle" },
|
||||
{ key: "/collections/:handle", prefix: "/collections/", param: "handle" },
|
||||
];
|
||||
import schema from "@/app.schema.json";
|
||||
|
||||
export type ResolvedRoute = {
|
||||
key: string;
|
||||
@@ -11,27 +6,59 @@ export type ResolvedRoute = {
|
||||
params: Record<string, string>;
|
||||
};
|
||||
|
||||
const resolveRoute = (slug: string[] = []): ResolvedRoute => {
|
||||
const path = slug.length === 0 ? "/" : `/${slug.join("/")}`;
|
||||
const ROUTE_KEYS = Object.keys(schema as Record<string, unknown>);
|
||||
|
||||
for (const { key, prefix, param } of TEMPLATE_PATTERNS) {
|
||||
if (path.startsWith(prefix) && path.length > prefix.length) {
|
||||
return { key, path, params: { [param]: path.slice(prefix.length) } };
|
||||
/**
|
||||
* Matches a concrete path's segments against a single express-style pattern
|
||||
* (e.g. `/products/:handle`). Returns the captured params, or null on mismatch.
|
||||
*/
|
||||
const matchPattern = (
|
||||
pattern: string,
|
||||
segments: string[],
|
||||
): Record<string, string> | null => {
|
||||
const patternSegments = pattern === "/" ? [] : pattern.slice(1).split("/");
|
||||
if (patternSegments.length !== segments.length) return null;
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
for (let i = 0; i < patternSegments.length; i++) {
|
||||
const part = patternSegments[i];
|
||||
if (part.startsWith(":")) {
|
||||
params[part.slice(1)] = segments[i];
|
||||
} else if (part !== segments[i]) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return { key: path, path, params: {} };
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the current route's `handle` param from the catch-all slug segments.
|
||||
* Use in client components rendered under `app/[[...slug]]` (and its editor
|
||||
* counterpart) where the handle is no longer exposed as a direct route param.
|
||||
* Resolves catch-all slug segments to a route key defined in the schema,
|
||||
* supporting any express-style pattern (`/products/:handle`, `/blog/:slug`,
|
||||
* etc.). Static segments are preferred over dynamic ones when both match.
|
||||
*/
|
||||
export const useRouteHandle = (): string | undefined => {
|
||||
const params = useParams();
|
||||
const slug = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
|
||||
return resolveRoute(slug).params.handle;
|
||||
const resolveRoute = (segments: string[] = []): ResolvedRoute => {
|
||||
// Editor routes live under `/editor/*`; the `editor` prefix is not part of
|
||||
// the schema route keys, so strip it before matching.
|
||||
const routeSegments =
|
||||
segments[0] === "editor" ? segments.slice(1) : segments;
|
||||
const path =
|
||||
routeSegments.length === 0 ? "/" : `/${routeSegments.join("/")}`;
|
||||
|
||||
let best: ResolvedRoute | null = null;
|
||||
let bestDynamicCount = Infinity;
|
||||
|
||||
for (const key of ROUTE_KEYS) {
|
||||
const params = matchPattern(key, routeSegments);
|
||||
if (!params) continue;
|
||||
|
||||
const dynamicCount = Object.keys(params).length;
|
||||
if (dynamicCount < bestDynamicCount) {
|
||||
best = { key, path, params };
|
||||
bestDynamicCount = dynamicCount;
|
||||
}
|
||||
}
|
||||
|
||||
return best ?? { key: path, path, params: {} };
|
||||
};
|
||||
|
||||
export default resolveRoute;
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@reacteditor/core": "0.0.32",
|
||||
"@reacteditor/field-google-fonts": "^0.0.3",
|
||||
"@reacteditor/field-shopify": "^0.0.2",
|
||||
"@reacteditor/plugin-ai": "0.0.8",
|
||||
"@reacteditor/plugin-media": "0.0.6",
|
||||
"@reacteditor/plugin-shopify": "^0.0.1",
|
||||
"@reacteditor/plugin-tailwind-cdn": "^0.0.3",
|
||||
|
||||
File diff suppressed because one or more lines are too long
427
vendor/plugin-ai.css
vendored
427
vendor/plugin-ai.css
vendored
@@ -1,427 +0,0 @@
|
||||
/* css-module:/Users/rami/Documents/apps/react-editor/packages/plugin-ai/src/panel/styles.module.css/#css-module-data */
|
||||
._AiPanel_1g0p6_1 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
._AiPanel-messages_1g0p6_8 {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
._AiPanel-messages_1g0p6_8::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
._AiPanel-messagesContent_1g0p6_21 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
._AiPanel-scrollDown_1g0p6_30 {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--editor-border-default, #e0e0e2);
|
||||
background: var(--editor-color-white, #fff);
|
||||
color: var(--editor-text-primary, #111);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
transition: background-color var(--editor-motion-fast) var(--editor-ease);
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
._AiPanel-scrollDown_1g0p6_30:hover {
|
||||
background: var(--editor-surface-hover, #f4f4f5);
|
||||
}
|
||||
}
|
||||
._AiPanel-empty_1g0p6_55 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--editor-text-tertiary, #888);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
._AiPanel-empty-icon_1g0p6_66 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--editor-text-secondary, #555);
|
||||
}
|
||||
._AiPanel-empty-text_1g0p6_73 {
|
||||
font-weight: 500;
|
||||
}
|
||||
._AiPanel-message_1g0p6_8 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
._AiPanel-message--user_1g0p6_85 {
|
||||
align-items: flex-end;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 {
|
||||
background: var(--editor-color-grey-12, #f4f4f5);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
max-width: 90%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
._AiPanel-message--user_1g0p6_85 ._AiPanel-message-bubble_1g0p6_89 {
|
||||
background: var(--editor-accent-soft);
|
||||
color: var(--editor-text-accent);
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 p,
|
||||
._AiPanel-message-bubble_1g0p6_89 ul,
|
||||
._AiPanel-message-bubble_1g0p6_89 ol,
|
||||
._AiPanel-message-bubble_1g0p6_89 li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 p + p,
|
||||
._AiPanel-message-bubble_1g0p6_89 p + ul,
|
||||
._AiPanel-message-bubble_1g0p6_89 p + ol,
|
||||
._AiPanel-message-bubble_1g0p6_89 p + pre,
|
||||
._AiPanel-message-bubble_1g0p6_89 p + blockquote,
|
||||
._AiPanel-message-bubble_1g0p6_89 p + table,
|
||||
._AiPanel-message-bubble_1g0p6_89 ul + p,
|
||||
._AiPanel-message-bubble_1g0p6_89 ol + p,
|
||||
._AiPanel-message-bubble_1g0p6_89 pre + p,
|
||||
._AiPanel-message-bubble_1g0p6_89 blockquote + p,
|
||||
._AiPanel-message-bubble_1g0p6_89 table + p {
|
||||
margin-top: 6px;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 ul,
|
||||
._AiPanel-message-bubble_1g0p6_89 ol {
|
||||
padding-left: 18px;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 li + li,
|
||||
._AiPanel-message-bubble_1g0p6_89 li > ul,
|
||||
._AiPanel-message-bubble_1g0p6_89 li > ol {
|
||||
margin-top: 0.375em;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 h1,
|
||||
._AiPanel-message-bubble_1g0p6_89 h2,
|
||||
._AiPanel-message-bubble_1g0p6_89 h3,
|
||||
._AiPanel-message-bubble_1g0p6_89 h4 {
|
||||
margin: 8px 0 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 h1 {
|
||||
font-size: 15px;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 h3,
|
||||
._AiPanel-message-bubble_1g0p6_89 h4 {
|
||||
font-size: 13px;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 blockquote {
|
||||
margin: 6px 0;
|
||||
padding: 0 8px;
|
||||
border-left: 2px solid var(--editor-border-default, #e0e0e2);
|
||||
color: var(--editor-text-secondary, #555);
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 pre {
|
||||
margin: 6px 0;
|
||||
padding: 8px 10px;
|
||||
background: var(--editor-color-grey-12, #f4f4f5);
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 code {
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
Consolas,
|
||||
monospace;
|
||||
font-size: 12px;
|
||||
background: var(--editor-color-grey-12, #f4f4f5);
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 table {
|
||||
margin: 6px 0;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 th,
|
||||
._AiPanel-message-bubble_1g0p6_89 td {
|
||||
border: 1px solid var(--editor-border-default, #e0e0e2);
|
||||
padding: 4px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 th {
|
||||
background: var(--editor-color-grey-12, #f4f4f5);
|
||||
font-weight: 600;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--editor-border-default, #e0e0e2);
|
||||
margin: 8px 0;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 a {
|
||||
color: var(--editor-text-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 em {
|
||||
font-style: italic;
|
||||
}
|
||||
._AiPanel-message-bubble_1g0p6_89 input[type=checkbox] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
._AiPanel-toolCall_1g0p6_220 {
|
||||
--shiny-width: 80px;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
font-size: 12px;
|
||||
color: var(--editor-text-tertiary, #888);
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
var(--editor-text-primary, #111) 50%,
|
||||
transparent) no-repeat;
|
||||
background-size: var(--shiny-width) 100%;
|
||||
background-position: calc(-100% - var(--shiny-width)) 0;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
animation: _AiPanel-shine_1g0p6_1 2.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes _AiPanel-shine_1g0p6_1 {
|
||||
0%, 90%, 100% {
|
||||
background-position: calc(-100% - var(--shiny-width)) 0;
|
||||
}
|
||||
30%, 60% {
|
||||
background-position: calc(100% + var(--shiny-width)) 0;
|
||||
}
|
||||
}
|
||||
._AiPanel-toolCall-done_1g0p6_249 {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
font-size: 12px;
|
||||
color: var(--editor-text-tertiary, #888);
|
||||
}
|
||||
._AiPanel-form_1g0p6_256 {
|
||||
padding: 10px;
|
||||
background: var(--editor-color-white, #fff);
|
||||
}
|
||||
._AiPanel-dots_1g0p6_261 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
._AiPanel-dots-dot_1g0p6_269 {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--editor-text-accent);
|
||||
animation: _AiPanel-dots-pulse_1g0p6_1 1.2s infinite ease-in-out;
|
||||
}
|
||||
@keyframes _AiPanel-dots-pulse_1g0p6_1 {
|
||||
0%, 80%, 100% {
|
||||
opacity: 0.2;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
._AiPanel-inputGroup_1g0p6_288 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--editor-border-default, #e0e0e2);
|
||||
border-radius: var(--editor-radius-md, 8px);
|
||||
background: var(--editor-color-white, #fff);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--editor-motion-fast) var(--editor-ease), box-shadow var(--editor-motion-fast) var(--editor-ease);
|
||||
}
|
||||
._AiPanel-inputGroup_1g0p6_288:focus-within {
|
||||
border-color: var(--editor-primary, #111);
|
||||
box-shadow: var(--editor-ring);
|
||||
}
|
||||
._AiPanel-input_1g0p6_288 {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
padding: 10px 12px 6px;
|
||||
min-height: 56px;
|
||||
max-height: 200px;
|
||||
field-sizing: content;
|
||||
}
|
||||
._AiPanel-inputGroup-actions_1g0p6_320 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
}
|
||||
._AiPanel-inputGroup-actions-left_1g0p6_328 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
._AiPanel-send_1g0p6_334 {
|
||||
appearance: none;
|
||||
border: 1px solid transparent;
|
||||
background: var(--editor-primary);
|
||||
color: var(--editor-primary-foreground);
|
||||
border-radius: var(--editor-radius-md, 6px);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background-color var(--editor-motion-fast) var(--editor-ease);
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
._AiPanel-send_1g0p6_334:hover {
|
||||
background: var(--editor-primary-hover);
|
||||
}
|
||||
}
|
||||
._AiPanel-send_1g0p6_334:active {
|
||||
background: var(--editor-primary-hover);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
._AiPanel-send_1g0p6_334:disabled {
|
||||
background: var(--editor-border-subtle);
|
||||
color: var(--editor-text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
._AiPanel-send_1g0p6_334 svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
._AiPanel-attach_1g0p6_372 {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--editor-text-secondary, #555);
|
||||
border-radius: var(--editor-radius-md, 6px);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background-color var(--editor-motion-fast) var(--editor-ease), color var(--editor-motion-fast) var(--editor-ease);
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
._AiPanel-attach_1g0p6_372:hover {
|
||||
background: var(--editor-surface-hover, #f4f4f5);
|
||||
color: var(--editor-text-primary, #111);
|
||||
}
|
||||
}
|
||||
._AiPanel-attach_1g0p6_372:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
._AiPanel-fileInput_1g0p6_401 {
|
||||
display: none;
|
||||
}
|
||||
._AiPanel-attachments_1g0p6_405 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 8px 0;
|
||||
}
|
||||
._AiPanel-attachment_1g0p6_405 {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--editor-color-grey-12, #f4f4f5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
._AiPanel-attachment-img_1g0p6_422 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
._AiPanel-attachment-remove_1g0p6_429 {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
._AiPanel-attachment-remove_1g0p6_429:hover {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
._AiPanel-userImage_1g0p6_451 {
|
||||
display: block;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
background: var(--editor-color-grey-12, #f4f4f5);
|
||||
}
|
||||
._AiPanel-loader_1g0p6_460 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: _AiPanel-loader-spin_1g0p6_1 1s linear infinite;
|
||||
}
|
||||
@keyframes _AiPanel-loader-spin_1g0p6_1 {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
._AiPanel-error_1g0p6_471 {
|
||||
color: var(--editor-color-red-05, #d44);
|
||||
font-size: 12px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
Reference in New Issue
Block a user