Feat: added website
@ -1,11 +1,10 @@
|
||||
|
||||
<div align="center">
|
||||
<a href="https://mysigma.ai">
|
||||
<img src="https://github.com/user-attachments/assets/3ae051f7-e77b-42b3-91d2-af69888e4d3f" width="200px" alt="Recall logo" />
|
||||
<img src="https://github.com/user-attachments/assets/3ae051f7-e77b-42b3-91d2-af69888e4d3f" width="200px" alt="CORE logo" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
# Recall
|
||||
# CORE
|
||||
|
||||
Simple memory management system for AI agents with per-space ingestion and search capabilities.
|
||||
|
||||
|
||||
303
apps/webapp/app/components/graph/graph-popover.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
"use client";
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import type { NodePopupContent, EdgePopupContent } from "./type";
|
||||
import { getNodeColor } from "./node-colors";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useTheme } from "remix-themes";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
|
||||
/**
|
||||
* Format a date string into a readable format
|
||||
*/
|
||||
export function formatDate(
|
||||
dateString?: string | null,
|
||||
format: string = "MMM D, YYYY",
|
||||
): string {
|
||||
if (!dateString) return "Unknown";
|
||||
|
||||
try {
|
||||
return dayjs(dateString).format(format);
|
||||
} catch (error) {
|
||||
console.error("Error formatting date:", error);
|
||||
return "Invalid date";
|
||||
}
|
||||
}
|
||||
|
||||
interface GraphPopoversProps {
|
||||
showNodePopup: boolean;
|
||||
showEdgePopup: boolean;
|
||||
nodePopupContent: NodePopupContent | null;
|
||||
edgePopupContent: EdgePopupContent | null;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
labelColorMap?: Map<string, number>;
|
||||
}
|
||||
|
||||
export function GraphPopovers({
|
||||
showNodePopup,
|
||||
showEdgePopup,
|
||||
nodePopupContent,
|
||||
edgePopupContent,
|
||||
onOpenChange,
|
||||
labelColorMap,
|
||||
}: GraphPopoversProps) {
|
||||
const [resolvedTheme] = useTheme();
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
const primaryNodeLabel = useMemo((): string | null => {
|
||||
if (!nodePopupContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if node has primaryLabel property (GraphNode)
|
||||
const nodeAny = nodePopupContent.node as any;
|
||||
if (nodeAny.primaryLabel && typeof nodeAny.primaryLabel === "string") {
|
||||
return nodeAny.primaryLabel;
|
||||
}
|
||||
|
||||
// Fall back to original logic with labels
|
||||
const primaryLabel = nodePopupContent.node.labels?.find(
|
||||
(label) => label !== "Entity",
|
||||
);
|
||||
return primaryLabel || "Entity";
|
||||
}, [nodePopupContent]);
|
||||
|
||||
// Get the color for the primary label
|
||||
const labelColor = useMemo(() => {
|
||||
if (!primaryNodeLabel || !labelColorMap) return "";
|
||||
return getNodeColor(primaryNodeLabel, isDarkMode, labelColorMap);
|
||||
}, [primaryNodeLabel, isDarkMode, labelColorMap]);
|
||||
|
||||
const attributesToDisplay = useMemo(() => {
|
||||
if (!nodePopupContent) {
|
||||
return [];
|
||||
}
|
||||
const entityProperties = Object.fromEntries(
|
||||
Object.entries(nodePopupContent.node.attributes || {}).filter(
|
||||
([key]) => key !== "labels",
|
||||
),
|
||||
);
|
||||
|
||||
return Object.entries(entityProperties).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
}));
|
||||
}, [nodePopupContent]);
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 right-4 z-50">
|
||||
<Popover open={showNodePopup} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="pointer-events-none h-4 w-4" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-80 overflow-hidden"
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={5}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<h4 className="leading-none font-medium">Node Details</h4>
|
||||
{primaryNodeLabel && (
|
||||
<span
|
||||
className="rounded-full px-2 py-1 text-xs font-medium text-white"
|
||||
style={{ backgroundColor: labelColor }}
|
||||
>
|
||||
{primaryNodeLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-muted-foreground text-sm break-all">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
Name:
|
||||
</span>
|
||||
{nodePopupContent?.node.name || "Unknown"}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm break-words">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
UUID:
|
||||
</span>
|
||||
{nodePopupContent?.node.uuid || "Unknown"}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm break-words">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
Created:
|
||||
</span>
|
||||
{nodePopupContent?.node.created_at &&
|
||||
formatDate(nodePopupContent?.node.created_at)}
|
||||
</p>
|
||||
|
||||
{attributesToDisplay.length > 0 && (
|
||||
<div className="border-border border-t pt-2">
|
||||
<p className="mb-2 text-sm font-medium text-black dark:text-white">
|
||||
Properties:
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{attributesToDisplay.map(({ key, value }) => (
|
||||
<p key={key} className="text-sm">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
{key}:
|
||||
</span>{" "}
|
||||
<span className="text-muted-foreground break-words">
|
||||
{typeof value === "object"
|
||||
? JSON.stringify(value)
|
||||
: String(value)}
|
||||
</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodePopupContent?.node.summary && (
|
||||
<div className="border-border border-t pt-2">
|
||||
<p className="mb-1 text-sm font-medium text-black dark:text-white">
|
||||
Summary:
|
||||
</p>
|
||||
<div
|
||||
className="relative max-h-[200px] overflow-y-auto"
|
||||
style={{
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "rgba(155, 155, 155, 0.5) transparent",
|
||||
pointerEvents: "auto",
|
||||
touchAction: "auto",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}
|
||||
onWheel={(e) => {
|
||||
e.stopPropagation();
|
||||
const target = e.currentTarget;
|
||||
target.scrollTop += e.deltaY;
|
||||
}}
|
||||
>
|
||||
<p className="text-muted-foreground pr-4 text-sm break-words">
|
||||
{nodePopupContent.node.summary}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodePopupContent?.node.labels?.length ? (
|
||||
<div className="border-border border-t pt-2">
|
||||
<p className="mb-1 text-sm font-medium text-black dark:text-white">
|
||||
Labels:
|
||||
</p>
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
{nodePopupContent.node.labels.map((label) => (
|
||||
<span
|
||||
key={label}
|
||||
className="bg-muted rounded-md px-2 py-1 text-xs"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover open={showEdgePopup} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="pointer-events-none h-4 w-4" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-80 overflow-hidden"
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={5}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="bg-muted mb-4 rounded-md p-2">
|
||||
<p className="text-sm break-all">
|
||||
{edgePopupContent?.source.name || "Unknown"} →{" "}
|
||||
<span className="font-medium">
|
||||
{edgePopupContent?.relation.name || "Unknown"}
|
||||
</span>{" "}
|
||||
→ {edgePopupContent?.target.name || "Unknown"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="leading-none font-medium">Relationship</h4>
|
||||
<div className="grid gap-2">
|
||||
<p className="text-muted-foreground text-sm break-all">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
UUID:
|
||||
</span>
|
||||
{edgePopupContent?.relation.uuid || "Unknown"}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm break-all">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
Type:
|
||||
</span>
|
||||
{edgePopupContent?.relation.name || "Unknown"}
|
||||
</p>
|
||||
{edgePopupContent?.relation.fact && (
|
||||
<p className="text-muted-foreground text-sm break-all">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
Fact:
|
||||
</span>
|
||||
{edgePopupContent.relation.fact}
|
||||
</p>
|
||||
)}
|
||||
{edgePopupContent?.relation.episodes?.length ? (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-black dark:text-white">
|
||||
Episodes:
|
||||
</p>
|
||||
<div className="mt-1 flex gap-2">
|
||||
{edgePopupContent.relation.episodes.map((episode) => (
|
||||
<span
|
||||
key={episode}
|
||||
className="bg-muted rounded-md px-2 py-1 text-xs"
|
||||
>
|
||||
{episode}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<p className="text-muted-foreground text-sm break-all">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
Created:
|
||||
</span>
|
||||
{formatDate(edgePopupContent?.relation.created_at)}
|
||||
</p>
|
||||
{edgePopupContent?.relation.valid_at && (
|
||||
<p className="text-muted-foreground text-sm break-all">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
Valid From:
|
||||
</span>
|
||||
{formatDate(edgePopupContent.relation.valid_at)}
|
||||
</p>
|
||||
)}
|
||||
{edgePopupContent?.relation.expired_at && (
|
||||
<p className="text-muted-foreground text-sm break-all">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
Expired At:
|
||||
</span>
|
||||
{formatDate(edgePopupContent.relation.expired_at)}
|
||||
</p>
|
||||
)}
|
||||
{edgePopupContent?.relation.invalid_at && (
|
||||
<p className="text-muted-foreground text-sm break-all">
|
||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||
Invalid At:
|
||||
</span>
|
||||
{formatDate(edgePopupContent.relation.invalid_at)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
185
apps/webapp/app/components/graph/graph-visualization.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, forwardRef } from "react";
|
||||
import { Graph, GraphRef } from "./graph";
|
||||
import { GraphPopovers } from "./graph-popover";
|
||||
import type { RawTriplet, NodePopupContent, EdgePopupContent } from "./type";
|
||||
import { toGraphTriplets } from "./type";
|
||||
import { createLabelColorMap, getNodeColor } from "./node-colors";
|
||||
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { useTheme } from "remix-themes";
|
||||
|
||||
interface GraphVisualizationProps {
|
||||
triplets: RawTriplet[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
zoomOnMount?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const GraphVisualization = forwardRef<GraphRef, GraphVisualizationProps>(
|
||||
(
|
||||
{
|
||||
triplets,
|
||||
width = window.innerWidth * 0.85,
|
||||
height = window.innerHeight * 0.85,
|
||||
zoomOnMount = true,
|
||||
className = "border border-border rounded-md h-[85vh] overflow-hidden relative",
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [resolvedTheme] = useTheme();
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
// Graph state for popovers
|
||||
const [showNodePopup, setShowNodePopup] = useState<boolean>(false);
|
||||
const [showEdgePopup, setShowEdgePopup] = useState<boolean>(false);
|
||||
const [nodePopupContent, setNodePopupContent] =
|
||||
useState<NodePopupContent | null>(null);
|
||||
const [edgePopupContent, setEdgePopupContent] =
|
||||
useState<EdgePopupContent | null>(null);
|
||||
|
||||
// Convert raw triplets to graph triplets
|
||||
const graphTriplets = useMemo(() => toGraphTriplets(triplets), [triplets]);
|
||||
|
||||
// Extract all unique labels from triplets
|
||||
const allLabels = useMemo(() => {
|
||||
const labels = new Set<string>();
|
||||
labels.add("Entity"); // Always include Entity as default
|
||||
|
||||
graphTriplets.forEach((triplet) => {
|
||||
if (triplet.source.primaryLabel)
|
||||
labels.add(triplet.source.primaryLabel);
|
||||
if (triplet.target.primaryLabel)
|
||||
labels.add(triplet.target.primaryLabel);
|
||||
});
|
||||
|
||||
return Array.from(labels).sort((a, b) => {
|
||||
// Always put "Entity" first
|
||||
if (a === "Entity") return -1;
|
||||
if (b === "Entity") return 1;
|
||||
// Sort others alphabetically
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}, [graphTriplets]);
|
||||
|
||||
// Create a shared label color map
|
||||
const sharedLabelColorMap = useMemo(() => {
|
||||
return createLabelColorMap(allLabels);
|
||||
}, [allLabels]);
|
||||
|
||||
// Handle node click
|
||||
const handleNodeClick = (nodeId: string) => {
|
||||
// Find the triplet that contains this node
|
||||
const triplet = triplets.find(
|
||||
(t) => t.sourceNode.uuid === nodeId || t.targetNode.uuid === nodeId,
|
||||
);
|
||||
|
||||
if (!triplet) return;
|
||||
|
||||
// Determine which node was clicked (source or target)
|
||||
const node =
|
||||
triplet.sourceNode.uuid === nodeId
|
||||
? triplet.sourceNode
|
||||
: triplet.targetNode;
|
||||
|
||||
// Set popup content and show the popup
|
||||
setNodePopupContent({
|
||||
id: nodeId,
|
||||
node: node,
|
||||
});
|
||||
setShowNodePopup(true);
|
||||
setShowEdgePopup(false);
|
||||
};
|
||||
|
||||
// Handle edge click
|
||||
const handleEdgeClick = (edgeId: string) => {
|
||||
// Find the triplet that contains this edge
|
||||
const triplet = triplets.find((t) => t.edge.uuid === edgeId);
|
||||
|
||||
if (!triplet) return;
|
||||
|
||||
// Set popup content and show the popup
|
||||
setEdgePopupContent({
|
||||
id: edgeId,
|
||||
source: triplet.sourceNode,
|
||||
target: triplet.targetNode,
|
||||
relation: triplet.edge,
|
||||
});
|
||||
setShowEdgePopup(true);
|
||||
setShowNodePopup(false);
|
||||
};
|
||||
|
||||
// Handle popover close
|
||||
const handlePopoverClose = () => {
|
||||
setShowNodePopup(false);
|
||||
setShowEdgePopup(false);
|
||||
};
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* Entity Types Legend Button */}
|
||||
<div className="absolute top-4 left-4 z-50">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<button className="bg-primary/10 text-primary hover:bg-primary/20 rounded-md px-2.5 py-1 text-xs transition-colors">
|
||||
Entity Types
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-40" side="bottom" align="start">
|
||||
<div className="space-y-2">
|
||||
<div className="max-h-[300px] space-y-1.5 overflow-y-auto pr-2">
|
||||
{allLabels.map((label) => (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-4 w-4 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: getNodeColor(
|
||||
label,
|
||||
isDarkMode,
|
||||
sharedLabelColorMap,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
|
||||
{triplets.length > 0 ? (
|
||||
<Graph
|
||||
ref={ref}
|
||||
triplets={graphTriplets}
|
||||
width={width}
|
||||
height={height}
|
||||
onNodeClick={handleNodeClick}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onBlur={handlePopoverClose}
|
||||
zoomOnMount={zoomOnMount}
|
||||
labelColorMap={sharedLabelColorMap}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-muted-foreground">No graph data to visualize.</p>
|
||||
</div>
|
||||
)}
|
||||
<GraphPopovers
|
||||
showNodePopup={showNodePopup}
|
||||
showEdgePopup={showEdgePopup}
|
||||
nodePopupContent={nodePopupContent}
|
||||
edgePopupContent={edgePopupContent}
|
||||
onOpenChange={handlePopoverClose}
|
||||
labelColorMap={sharedLabelColorMap}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
1051
apps/webapp/app/components/graph/graph.tsx
Normal file
84
apps/webapp/app/components/graph/node-colors.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import colors from "tailwindcss/colors";
|
||||
|
||||
// Define a color palette for node coloring
|
||||
export const nodeColorPalette = {
|
||||
light: [
|
||||
colors.pink[500], // Entity (default)
|
||||
colors.blue[500],
|
||||
colors.emerald[500],
|
||||
colors.amber[500],
|
||||
colors.indigo[500],
|
||||
colors.orange[500],
|
||||
colors.teal[500],
|
||||
colors.purple[500],
|
||||
colors.cyan[500],
|
||||
colors.lime[500],
|
||||
colors.rose[500],
|
||||
colors.violet[500],
|
||||
colors.green[500],
|
||||
colors.red[500],
|
||||
],
|
||||
dark: [
|
||||
colors.pink[400], // Entity (default)
|
||||
colors.blue[400],
|
||||
colors.emerald[400],
|
||||
colors.amber[400],
|
||||
colors.indigo[400],
|
||||
colors.orange[400],
|
||||
colors.teal[400],
|
||||
colors.purple[400],
|
||||
colors.cyan[400],
|
||||
colors.lime[400],
|
||||
colors.rose[400],
|
||||
colors.violet[400],
|
||||
colors.green[400],
|
||||
colors.red[400],
|
||||
],
|
||||
};
|
||||
|
||||
// Function to create a map of label to color index
|
||||
export function createLabelColorMap(labels: string[]) {
|
||||
// Start with Entity mapped to first color
|
||||
const result = new Map<string, number>();
|
||||
result.set("Entity", 0);
|
||||
|
||||
// Sort all non-Entity labels alphabetically for consistent color assignment
|
||||
const sortedLabels = labels
|
||||
.filter((label) => label !== "Entity")
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
// Map each unique label to a color index
|
||||
let nextIndex = 1;
|
||||
sortedLabels.forEach((label) => {
|
||||
if (!result.has(label)) {
|
||||
result.set(label, nextIndex % nodeColorPalette.light.length);
|
||||
nextIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get color for a label directly
|
||||
export function getNodeColor(
|
||||
label: string | null | undefined,
|
||||
isDarkMode: boolean,
|
||||
labelColorMap: Map<string, number>,
|
||||
): string {
|
||||
if (!label) {
|
||||
return isDarkMode ? nodeColorPalette.dark[0] : nodeColorPalette.light[0];
|
||||
}
|
||||
|
||||
// If label is "Entity" or not found in the map, return default color
|
||||
if (label === "Entity" || !labelColorMap.has(label)) {
|
||||
return isDarkMode ? nodeColorPalette.dark[0] : nodeColorPalette.light[0];
|
||||
}
|
||||
|
||||
// Get the color index for this label
|
||||
const colorIndex = labelColorMap.get(label) || 0;
|
||||
|
||||
// Return the color from the appropriate theme palette
|
||||
return isDarkMode
|
||||
? nodeColorPalette.dark[colorIndex]
|
||||
: nodeColorPalette.light[colorIndex];
|
||||
}
|
||||
82
apps/webapp/app/components/graph/type.ts
Normal file
@ -0,0 +1,82 @@
|
||||
export interface Node {
|
||||
uuid: string;
|
||||
name: string;
|
||||
summary?: string;
|
||||
labels?: string[];
|
||||
attributes?: Record<string, any>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
uuid: string;
|
||||
source_node_uuid: string;
|
||||
target_node_uuid: string;
|
||||
type: string;
|
||||
name: string;
|
||||
fact?: string;
|
||||
episodes?: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
valid_at?: string;
|
||||
expired_at?: string;
|
||||
invalid_at?: string;
|
||||
}
|
||||
|
||||
export interface RawTriplet {
|
||||
sourceNode: Node;
|
||||
edge: Edge;
|
||||
targetNode: Node;
|
||||
}
|
||||
|
||||
export interface GraphNode extends Node {
|
||||
id: string;
|
||||
value: string;
|
||||
primaryLabel?: string;
|
||||
}
|
||||
|
||||
export interface GraphEdge extends Edge {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface GraphTriplet {
|
||||
source: GraphNode;
|
||||
relation: GraphEdge;
|
||||
target: GraphNode;
|
||||
}
|
||||
|
||||
export interface IdValue {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// Graph visualization types
|
||||
export interface GraphNode extends Node {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface GraphEdge extends Edge {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface GraphTriplet {
|
||||
source: GraphNode;
|
||||
relation: GraphEdge;
|
||||
target: GraphNode;
|
||||
}
|
||||
|
||||
// Popup content types for UI
|
||||
export interface NodePopupContent {
|
||||
id: string;
|
||||
node: Node;
|
||||
}
|
||||
|
||||
export interface EdgePopupContent {
|
||||
id: string;
|
||||
source: Node;
|
||||
relation: Edge;
|
||||
target: Node;
|
||||
}
|
||||
@ -7,9 +7,9 @@ export function LoginPageLayout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center">
|
||||
<div className="pt-8">
|
||||
<Logo width={5} height={5} />
|
||||
<Button onClick={() => setTheme(Theme.DARK)}>theme</Button>
|
||||
<div className="text-foreground flex flex-col items-center pt-8 font-mono">
|
||||
<Logo width={50} height={50} />
|
||||
C.O.R.E
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-grow items-center justify-center">
|
||||
|
||||
70
apps/webapp/app/components/sidebar/app-sidebar.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "../ui/sidebar";
|
||||
import { DashboardIcon } from "@radix-ui/react-icons";
|
||||
import { Code, LucideFileStack } from "lucide-react";
|
||||
import { NavMain } from "./nav-main";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { NavUser } from "./nav-user";
|
||||
import { useWorkspace } from "~/hooks/useWorkspace";
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "#",
|
||||
icon: DashboardIcon,
|
||||
},
|
||||
{
|
||||
title: "API",
|
||||
url: "#",
|
||||
icon: Code,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const user = useUser();
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="offcanvas" {...props} className="bg-background">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||
>
|
||||
<a href="#">
|
||||
<span className="text-base font-semibold">
|
||||
{workspace.name}
|
||||
</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
38
apps/webapp/app/components/sidebar/nav-main.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import {
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "../ui/sidebar";
|
||||
import { NavUser } from "./nav-user";
|
||||
|
||||
export const NavMain = ({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: any;
|
||||
}[];
|
||||
}) => {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu></SidebarMenu>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
};
|
||||
83
apps/webapp/app/components/sidebar/nav-user.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { DotIcon, LogOut, User as UserI } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage, AvatarText } from "../ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "../ui/sidebar";
|
||||
import type { User } from "~/models/user.server";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
|
||||
export function NavUser({ user }: { user: User }) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<AvatarText
|
||||
text={user.name ?? "User"}
|
||||
className="h-6 w-6 rounded"
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{user.name}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<UserI />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
87
apps/webapp/app/components/ui/avatar.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import React from "react";
|
||||
|
||||
import { getTailwindColor } from "./color-utils";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-[20px] w-[20px] shrink-0 overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-muted flex h-full w-full items-center justify-center text-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
|
||||
// Function to get the first two letters
|
||||
export const getInitials = (name: string, noOfChar?: number | undefined) => {
|
||||
if (!name) {
|
||||
return "";
|
||||
}
|
||||
const words = name.split(" ");
|
||||
return words
|
||||
.map((word) => word.charAt(0))
|
||||
.filter((char) => char !== "")
|
||||
.slice(0, noOfChar ? noOfChar : 2)
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
};
|
||||
|
||||
const AvatarText = ({
|
||||
text,
|
||||
className,
|
||||
noOfChar,
|
||||
}: {
|
||||
text: string;
|
||||
className?: string;
|
||||
noOfChar?: number;
|
||||
}) => {
|
||||
return (
|
||||
<Avatar className={cn("flex items-center", className)}>
|
||||
<AvatarImage />
|
||||
<AvatarFallback
|
||||
className="rounded-sm"
|
||||
style={{
|
||||
background: getTailwindColor(text),
|
||||
}}
|
||||
>
|
||||
{getInitials(text, noOfChar)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback, AvatarText };
|
||||
@ -11,7 +11,8 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-white shadow hover:bg-primary/90 dark:hover:bg-primary/90",
|
||||
default:
|
||||
"bg-primary text-white shadow hover:bg-primary/90 dark:hover:bg-primary/90",
|
||||
destructive: "text-red-500 bg-grayAlpha-100 border-none",
|
||||
outline: "border border-border shadow-sm hover:bg-gray-100 shadow-none",
|
||||
secondary: "bg-grayAlpha-100 border-none",
|
||||
@ -37,7 +38,7 @@ const buttonVariants = cva(
|
||||
size: "default",
|
||||
full: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
@ -62,7 +63,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
@ -70,7 +71,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
<Comp
|
||||
className={cn(
|
||||
buttonVariants({ variant, size, full, className }),
|
||||
isActive && "bg-accent text-accent-foreground"
|
||||
isActive && "bg-accent text-accent-foreground",
|
||||
)}
|
||||
ref={ref}
|
||||
type="button"
|
||||
@ -81,7 +82,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
|
||||
80
apps/webapp/app/components/ui/card.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("bg-background-3 text-foreground rounded", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn(className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-3 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
43
apps/webapp/app/components/ui/color-utils.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Generates an OKLCH color string with fixed lightness, chroma, and a random hue.
|
||||
* @returns {string} - The generated OKLCH color string.
|
||||
*/
|
||||
export function generateOklchColor(): string {
|
||||
// Generate a random number between 30 and 360 for the hue
|
||||
const hue = Math.floor(Math.random() * (360 - 30 + 1)) + 30;
|
||||
|
||||
// Fixed lightness and chroma values
|
||||
const lightness = 66;
|
||||
const chroma = 0.1835;
|
||||
|
||||
// Construct the OKLCH color string
|
||||
const oklchColor = `oklch(${lightness}% ${chroma} ${hue})`;
|
||||
|
||||
return oklchColor;
|
||||
}
|
||||
|
||||
export function getTailwindColor(name: string): string {
|
||||
// Generate a hash value for the input name
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
// Ensure hash value is within the range of colors array
|
||||
const index = Math.abs(hash) % 12;
|
||||
|
||||
return `var(--custom-color-${index + 1})`;
|
||||
}
|
||||
|
||||
export function getTeamColor(name: string): string {
|
||||
// Generate a hash value for the input name
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
// Ensure hash value is within the range of colors array
|
||||
const index = Math.abs(hash) % 3;
|
||||
|
||||
return `var(--team-color-${index + 1})`;
|
||||
}
|
||||
204
apps/webapp/app/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DotFilledIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent flex cursor-default items-center rounded-sm px-2 py-1.5 outline-none select-none",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-3 w-3" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground shadow-1 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 font-sans",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1 font-sans transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 font-sans transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 font-sans transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-4 w-4 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 font-sans font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
25
apps/webapp/app/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"bg-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-8 w-full rounded px-3 py-1 transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
76
apps/webapp/app/components/ui/multi-select.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "./button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "./dropdown-menu";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
interface ISelectProps {
|
||||
placeholder: string;
|
||||
options: Option[];
|
||||
selectedOptions: string[];
|
||||
setSelectedOptions: Dispatch<SetStateAction<string[]>>;
|
||||
}
|
||||
const MultiSelect = ({
|
||||
placeholder,
|
||||
options: values,
|
||||
selectedOptions: selectedItems,
|
||||
setSelectedOptions: setSelectedItems,
|
||||
}: ISelectProps) => {
|
||||
const handleSelectChange = (value: string) => {
|
||||
if (!selectedItems.includes(value)) {
|
||||
setSelectedItems((prev) => [...prev, value]);
|
||||
} else {
|
||||
const referencedArray = [...selectedItems];
|
||||
const indexOfItemToBeRemoved = referencedArray.indexOf(value);
|
||||
referencedArray.splice(indexOfItemToBeRemoved, 1);
|
||||
setSelectedItems(referencedArray);
|
||||
}
|
||||
};
|
||||
|
||||
const isOptionSelected = (value: string): boolean => {
|
||||
return selectedItems.includes(value) ? true : false;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex h-8 w-full items-center justify-between"
|
||||
>
|
||||
<div className="text-muted-foreground">{placeholder}</div>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56 border-none text-base"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
{values.map((value: ISelectProps["options"][0], index: number) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
key={index}
|
||||
checked={isOptionSelected(value.value)}
|
||||
onCheckedChange={() => handleSelectChange(value.value)}
|
||||
>
|
||||
{value.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
38
apps/webapp/app/components/ui/popover.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||
const PopoverPortal = PopoverPrimitive.Portal;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"shadow-1 bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md p-4 font-sans outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverAnchor,
|
||||
PopoverPortal,
|
||||
};
|
||||
174
apps/webapp/app/components/ui/select.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
interface SelectTriggerProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {
|
||||
showIcon: boolean;
|
||||
}
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
SelectTriggerProps
|
||||
>(({ className, children, showIcon = true, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-8 w-full items-center justify-between rounded px-2 py-2 whitespace-nowrap focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showIcon && (
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
)}
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"shadow-1 bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border-[#ffffff38] font-sans",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-muted-foreground px-2 py-1.5 text-sm font-semibold",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[selected]:text-accent-foreground relative flex w-full cursor-default items-center gap-1 rounded-sm py-1 pr-8 pl-2 outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText className="flex gap-1">
|
||||
{children}
|
||||
</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
29
apps/webapp/app/components/ui/separator.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0",
|
||||
orientation === "horizontal" ? "h-[0.5px] w-full" : "h-full w-[0.5px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
138
apps/webapp/app/components/ui/sheet.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 font-sans",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), "font-sans", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-foreground text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
724
apps/webapp/app/components/ui/sidebar.tsx
Normal file
@ -0,0 +1,724 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "~/hooks/use-mobile";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "~/components/ui/sheet";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-background text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-background group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
asChild = false,
|
||||
size = "md",
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
17
apps/webapp/app/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("bg-grayAlpha-200 animate-pulse rounded", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
28
apps/webapp/app/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden border rounded bg-background-3 px-3 py-1.5 text-xs text-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@ -6,7 +6,7 @@ import {
|
||||
type PrismaTransactionClient,
|
||||
type PrismaTransactionOptions,
|
||||
$transaction as transac,
|
||||
} from "@recall/database";
|
||||
} from "@core/database";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { env } from "./env.server";
|
||||
@ -214,7 +214,7 @@ function redactUrlSecrets(hrefOrUrl: string | URL) {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
export type { PrismaClient } from "@recall/database";
|
||||
export type { PrismaClient } from "@core/database";
|
||||
|
||||
export const PrismaErrorSchema = z.object({
|
||||
code: z.string(),
|
||||
|
||||
19
apps/webapp/app/hooks/use-mobile.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
21
apps/webapp/app/hooks/use-mobile.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@ -33,7 +33,7 @@ export function useTypedMatchesData<T = AppData>({
|
||||
}
|
||||
|
||||
export function useTypedMatchData<T = AppData>(
|
||||
match: UIMatch | undefined
|
||||
match: UIMatch | undefined,
|
||||
): UseDataFunctionReturn<T> | undefined {
|
||||
if (!match) {
|
||||
return undefined;
|
||||
|
||||
@ -5,7 +5,7 @@ import { useChanged } from "./useChanged";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export function useIsImpersonating(matches?: UIMatch[]) {
|
||||
const data = useTypedMatchesData<typeof orgLoader>({
|
||||
const data = useTypedMatchesData({
|
||||
id: "routes/_app.workspace.$workspaceSlug",
|
||||
matches,
|
||||
});
|
||||
@ -25,7 +25,7 @@ export function useUser(matches?: UIMatch[]): User {
|
||||
const maybeUser = useOptionalUser(matches);
|
||||
if (!maybeUser) {
|
||||
throw new Error(
|
||||
"No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead."
|
||||
"No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead.",
|
||||
);
|
||||
}
|
||||
return maybeUser;
|
||||
|
||||
25
apps/webapp/app/hooks/useWorkspace.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { type Workspace } from "@core/database";
|
||||
import { type UIMatch } from "@remix-run/react";
|
||||
import { type loader } from "~/routes/_index";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export function useOptionalWorkspace(
|
||||
matches?: UIMatch[],
|
||||
): Workspace | undefined {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: "routes/_index",
|
||||
matches,
|
||||
}) as any;
|
||||
|
||||
return routeMatch?.workspace ?? undefined;
|
||||
}
|
||||
|
||||
export function useWorkspace(matches?: UIMatch[]): Workspace {
|
||||
const maybeWorkspace = useOptionalWorkspace(matches);
|
||||
if (!maybeWorkspace) {
|
||||
throw new Error(
|
||||
"No workspace found in root loader, but Workspace is required by useWorkspace. If Workspace is optional, try useOptionalWorkspace instead.",
|
||||
);
|
||||
}
|
||||
return maybeWorkspace;
|
||||
}
|
||||
@ -2,8 +2,9 @@ import {
|
||||
json,
|
||||
type Session,
|
||||
createCookieSessionStorage,
|
||||
redirect,
|
||||
} from "@remix-run/node";
|
||||
import { redirect } from "remix-typedjson";
|
||||
|
||||
import { env } from "~/env.server";
|
||||
import { createThemeSessionResolver } from "remix-themes";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type PersonalAccessToken } from "@recall/database";
|
||||
import { type PersonalAccessToken } from "@core/database";
|
||||
import { prisma } from "~/db.server";
|
||||
import nodeCrypto from "node:crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { Prisma, User } from "@recall/database";
|
||||
import type { Prisma, User } from "@core/database";
|
||||
import type { GoogleProfile } from "@coji/remix-auth-google";
|
||||
import { prisma } from "~/db.server";
|
||||
export type { User } from "@recall/database";
|
||||
export type { User } from "@core/database";
|
||||
|
||||
type FindOrCreateGoogle = {
|
||||
authenticationMethod: "GOOGLE";
|
||||
|
||||
38
apps/webapp/app/models/workspace.server.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { type Workspace } from "@core/database";
|
||||
import { prisma } from "~/db.server";
|
||||
|
||||
interface CreateWorkspaceDto {
|
||||
slug: string;
|
||||
name: string;
|
||||
integrations: string[];
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export async function createWorkspace(
|
||||
input: CreateWorkspaceDto,
|
||||
): Promise<Workspace> {
|
||||
const workspace = await prisma.workspace.create({
|
||||
data: {
|
||||
slug: input.slug,
|
||||
name: input.name,
|
||||
userId: input.userId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: input.userId },
|
||||
data: {
|
||||
confirmedBasicDetails: true,
|
||||
},
|
||||
});
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
export async function getWorkspaceByUser(userId: string) {
|
||||
return await prisma.workspace.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -37,6 +37,7 @@ import { RouteErrorDisplay } from "./components/ErrorDisplay";
|
||||
import { themeSessionResolver } from "./services/sessionStorage.server";
|
||||
import {
|
||||
PreventFlashOnWrongTheme,
|
||||
Theme,
|
||||
ThemeProvider,
|
||||
useTheme,
|
||||
} from "remix-themes";
|
||||
@ -53,6 +54,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
|
||||
return typedjson(
|
||||
{
|
||||
user: await getUser(request),
|
||||
toastMessage,
|
||||
theme: getTheme(),
|
||||
posthogProjectKey,
|
||||
@ -67,7 +69,7 @@ export const meta: MetaFunction = ({ data }) => {
|
||||
const typedData = data as UseDataFunctionReturn<typeof loader>;
|
||||
|
||||
return [
|
||||
{ title: `Recall${typedData && appEnvTitleTag(typedData.appEnv)}` },
|
||||
{ title: `CORE${typedData && appEnvTitleTag(typedData.appEnv)}` },
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=1024, initial-scale=1",
|
||||
@ -76,7 +78,7 @@ export const meta: MetaFunction = ({ data }) => {
|
||||
name: "robots",
|
||||
content:
|
||||
typeof window === "undefined" ||
|
||||
window.location.hostname !== "recall.mysigma.ai"
|
||||
window.location.hostname !== "core.mysigma.ai"
|
||||
? "noindex, nofollow"
|
||||
: "index, follow",
|
||||
},
|
||||
@ -119,7 +121,7 @@ function App() {
|
||||
<Links />
|
||||
<PreventFlashOnWrongTheme ssrTheme={Boolean(theme)} />
|
||||
</head>
|
||||
<body className="bg-background h-full overflow-hidden">
|
||||
<body className="bg-background h-full overflow-hidden font-sans">
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
|
||||
@ -137,7 +139,7 @@ function App() {
|
||||
export default function AppWithProviders() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
return (
|
||||
<ThemeProvider specifiedTheme={data.theme} themeAction="/action/set-theme">
|
||||
<ThemeProvider specifiedTheme={Theme.LIGHT} themeAction="/action/set-theme">
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@ -1,139 +1,60 @@
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { createPersonalAccessToken } from "~/services/personalAccessToken.server";
|
||||
import { redirect, type MetaFunction } from "@remix-run/node";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import { AppSidebar } from "~/components/sidebar/app-sidebar";
|
||||
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
|
||||
import { clearRedirectTo, commitSession } from "~/services/redirectTo.server";
|
||||
|
||||
import { requireUser, requireWorkpace } from "~/services/session.server";
|
||||
import { confirmBasicDetailsPath } from "~/utils/pathBuilder";
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "New Remix App" },
|
||||
{ name: "description", content: "Welcome to Remix!" },
|
||||
{ title: "C.O.R.E" },
|
||||
{ name: "description", content: "Welcome to C.O.R.E!" },
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
//you have to confirm basic details before you can do anything
|
||||
if (!user.confirmedBasicDetails) {
|
||||
return redirect(confirmBasicDetailsPath());
|
||||
}
|
||||
|
||||
const workspace = await requireWorkpace(request);
|
||||
|
||||
return typedjson(
|
||||
{
|
||||
workspace,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(await clearRedirectTo(request)),
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-16">
|
||||
<header className="flex flex-col items-center gap-9">
|
||||
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
|
||||
Welcome to <span className="sr-only">Remix</span>
|
||||
</h1>
|
||||
<div className="h-[144px] w-[434px]">
|
||||
<img
|
||||
src="/logo-light.png"
|
||||
alt="Remix"
|
||||
className="block w-full dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/logo-dark.png"
|
||||
alt="Remix"
|
||||
className="hidden w-full dark:block"
|
||||
/>
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 54)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
background: "var(--background)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar variant="inset" />
|
||||
<SidebarInset className="bg-background-2">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"></div>
|
||||
</div>
|
||||
</header>
|
||||
<nav className="flex flex-col items-center justify-center gap-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
|
||||
<p className="leading-6 text-gray-700 dark:text-gray-200">
|
||||
What's next?
|
||||
</p>
|
||||
<ul>
|
||||
{resources.map(({ href, text, icon }) => (
|
||||
<li key={href}>
|
||||
<a
|
||||
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{icon}
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const resources = [
|
||||
{
|
||||
href: "https://remix.run/start/quickstart",
|
||||
text: "Quick Start (5 min)",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||
>
|
||||
<path
|
||||
d="M8.51851 12.0741L7.92592 18L15.6296 9.7037L11.4815 7.33333L12.0741 2L4.37036 10.2963L8.51851 12.0741Z"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "https://remix.run/start/tutorial",
|
||||
text: "Tutorial (30 min)",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||
>
|
||||
<path
|
||||
d="M4.561 12.749L3.15503 14.1549M3.00811 8.99944H1.01978M3.15503 3.84489L4.561 5.2508M8.3107 1.70923L8.3107 3.69749M13.4655 3.84489L12.0595 5.2508M18.1868 17.0974L16.635 18.6491C16.4636 18.8205 16.1858 18.8205 16.0144 18.6491L13.568 16.2028C13.383 16.0178 13.0784 16.0347 12.915 16.239L11.2697 18.2956C11.047 18.5739 10.6029 18.4847 10.505 18.142L7.85215 8.85711C7.75756 8.52603 8.06365 8.21994 8.39472 8.31453L17.6796 10.9673C18.0223 11.0653 18.1115 11.5094 17.8332 11.7321L15.7766 13.3773C15.5723 13.5408 15.5554 13.8454 15.7404 14.0304L18.1868 16.4767C18.3582 16.6481 18.3582 16.926 18.1868 17.0974Z"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "https://remix.run/docs",
|
||||
text: "Remix Docs",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||
>
|
||||
<path
|
||||
d="M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "https://rmx.as/discord",
|
||||
text: "Join Discord",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="20"
|
||||
viewBox="0 0 24 20"
|
||||
fill="none"
|
||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||
>
|
||||
<path
|
||||
d="M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
151
apps/webapp/app/routes/confirm-basic-details.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { z } from "zod";
|
||||
import { useActionData } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, json } from "@remix-run/node";
|
||||
import { useForm } from "@conform-to/react";
|
||||
import { getFieldsetConstraint, parse } from "@conform-to/zod";
|
||||
import { LoginPageLayout } from "~/components/layout/LoginPageLayout";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { useState } from "react";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { rootPath } from "~/utils/pathBuilder";
|
||||
import { createWorkspace } from "~/models/workspace.server";
|
||||
|
||||
const schema = z.object({
|
||||
workspaceName: z
|
||||
.string()
|
||||
.min(3, "Your workspace name must be at least 3 characters")
|
||||
.max(50),
|
||||
workspaceSlug: z
|
||||
.string()
|
||||
.min(3, "Your workspace slug must be at least 3 characters")
|
||||
.max(50),
|
||||
});
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
const submission = parse(formData, { schema });
|
||||
|
||||
if (!submission.value || submission.intent !== "submit") {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
const { workspaceSlug, workspaceName } = submission.value;
|
||||
|
||||
try {
|
||||
await createWorkspace({
|
||||
slug: workspaceSlug,
|
||||
integrations: [],
|
||||
name: workspaceName,
|
||||
userId,
|
||||
});
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
rootPath(),
|
||||
request,
|
||||
"Your details have been updated.",
|
||||
);
|
||||
} catch (e: any) {
|
||||
return json({ errors: { body: e.message } }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export default function ConfirmBasicDetails() {
|
||||
const lastSubmission = useActionData<typeof action>();
|
||||
const [selectedApps, setSelectedApps] = useState<string[]>([]);
|
||||
|
||||
const [form, fields] = useForm({
|
||||
lastSubmission: lastSubmission as any,
|
||||
constraint: getFieldsetConstraint(schema),
|
||||
onValidate({ formData }) {
|
||||
console.log(parse(formData, { schema }));
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
onSubmit(event, context) {
|
||||
console.log(event);
|
||||
},
|
||||
defaultValue: {
|
||||
integrations: [],
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<LoginPageLayout>
|
||||
<Card className="min-w-[500px] rounded-lg p-3 pt-1">
|
||||
<CardHeader className="flex flex-col items-start px-0">
|
||||
<CardTitle className="px-0">Onboarding</CardTitle>
|
||||
<CardDescription>
|
||||
We just need you to confirm a couple of details, it'll only take a
|
||||
minute.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-2 text-base">
|
||||
<form method="post" {...form.props}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="workspaceName"
|
||||
className="text-muted-foreground mb-1 block text-sm"
|
||||
>
|
||||
Workspace Name
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="workspaceName"
|
||||
placeholder="Workspace name"
|
||||
name={fields.workspaceName.name}
|
||||
className="mt-1 block w-full text-base"
|
||||
/>
|
||||
{fields.workspaceName.error && (
|
||||
<div className="text-sm text-red-500">
|
||||
{fields.workspaceName.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="workspaceSlug"
|
||||
className="text-muted-foreground mb-1 block text-sm"
|
||||
>
|
||||
Workspace Slug
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="workspaceSlug"
|
||||
placeholder="Give unique workspace slug"
|
||||
name={fields.workspaceSlug.name}
|
||||
className="mt-1 block w-full text-base"
|
||||
/>
|
||||
{fields.workspaceSlug.error && (
|
||||
<div className="text-sm text-red-500">
|
||||
{fields.workspaceSlug.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
className="rounded-lg px-4 py-2"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</LoginPageLayout>
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import { EpisodeType } from "@recall/types";
|
||||
import { json } from "@remix-run/node";
|
||||
import { EpisodeType } from "@core/types";
|
||||
import { json, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { getUserQueue } from "~/lib/ingest.queue";
|
||||
import { prisma } from "~/db.server";
|
||||
import { IngestionStatus } from "@recall/database";
|
||||
import { IngestionStatus } from "@core/database";
|
||||
|
||||
export const IngestBodyRequest = z.object({
|
||||
name: z.string(),
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Form } from "@remix-run/react";
|
||||
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { LoginPageLayout } from "~/components/layout/LoginPageLayout";
|
||||
import { Fieldset } from "~/components/ui/Fieldset";
|
||||
import { Header1 } from "~/components/ui/Headers";
|
||||
import { Paragraph } from "~/components/ui/Paragraph";
|
||||
import { isGoogleAuthSupported } from "~/services/auth.server";
|
||||
import { setRedirectTo } from "~/services/redirectTo.server";
|
||||
import { getUserId } from "~/services/session.server";
|
||||
@ -12,6 +10,14 @@ import { commitSession } from "~/services/sessionStorage.server";
|
||||
import { requestUrl } from "~/utils/requestUrl.server";
|
||||
|
||||
import { RiGoogleLine } from "@remixicon/react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const userId = await getUserId(request);
|
||||
@ -43,29 +49,33 @@ export default function LoginPage() {
|
||||
const data = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<Form
|
||||
action={`/auth/google${data.redirectTo ? `?redirectTo=${data.redirectTo}` : ""}`}
|
||||
method="GET"
|
||||
className="w-full"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<Header1 className="pb-4 font-semibold sm:text-2xl md:text-3xl lg:text-4xl">
|
||||
Welcome
|
||||
</Header1>
|
||||
<Paragraph variant="base" className="mb-6">
|
||||
Create an account or login
|
||||
</Paragraph>
|
||||
<Fieldset className="w-full">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{data.showGoogleAuth && (
|
||||
<button type="submit" data-action="continue with google">
|
||||
<RiGoogleLine className={"mr-2 size-5"} />
|
||||
<span className="text-text-bright">Continue with Google</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Fieldset>
|
||||
</div>
|
||||
</Form>
|
||||
<LoginPageLayout>
|
||||
<Card className="min-w-[300px] rounded-md p-3">
|
||||
<CardHeader className="flex flex-col items-start">
|
||||
<CardTitle>Login to your account</CardTitle>
|
||||
<CardDescription>Create an account or login</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-2">
|
||||
<Fieldset className="w-full">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{data.showGoogleAuth && (
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
className="rounded-lg text-base"
|
||||
data-action="continue with google"
|
||||
onClick={() => (window.location.href = "/auth/google")}
|
||||
>
|
||||
<RiGoogleLine className={"mr-1 size-5"} />
|
||||
<span>Continue with Google</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Fieldset>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</LoginPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
18
apps/webapp/app/routes/logout.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { type ActionFunction, type LoaderFunction } from "@remix-run/node";
|
||||
import { redirect } from "remix-typedjson";
|
||||
|
||||
import { sessionStorage } from "~/services/sessionStorage.server";
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
let session = await sessionStorage.getSession(request.headers.get("cookie"));
|
||||
return redirect("/login", {
|
||||
headers: { "Set-Cookie": await sessionStorage.destroySession(session) },
|
||||
});
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
let session = await sessionStorage.getSession(request.headers.get("cookie"));
|
||||
return redirect("/login", {
|
||||
headers: { "Set-Cookie": await sessionStorage.destroySession(session) },
|
||||
});
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import type { EntityNode } from "@recall/types";
|
||||
import type { EntityNode } from "@core/types";
|
||||
import { runQuery } from "~/lib/neo4j.server";
|
||||
|
||||
export async function saveEntity(entity: EntityNode): Promise<string> {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { runQuery } from "~/lib/neo4j.server";
|
||||
import type { EpisodicNode } from "@recall/types";
|
||||
import type { EpisodicNode } from "@core/types";
|
||||
|
||||
export async function saveEpisode(episode: EpisodicNode): Promise<string> {
|
||||
const query = `
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
EpisodicNode,
|
||||
StatementNode,
|
||||
Triple,
|
||||
} from "@recall/types";
|
||||
} from "@core/types";
|
||||
import { runQuery } from "~/lib/neo4j.server";
|
||||
import { saveEntity } from "./entity";
|
||||
import { saveEpisode } from "./episode";
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
type EpisodicNode,
|
||||
type StatementNode,
|
||||
type Triple,
|
||||
} from "@recall/types";
|
||||
} from "@core/types";
|
||||
import { logger } from "./logger.service";
|
||||
import crypto from "crypto";
|
||||
import { dedupeNodes, extractMessage, extractText } from "./prompts/nodes";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type PersonalAccessToken } from "@recall/database";
|
||||
import { type PersonalAccessToken } from "@core/database";
|
||||
import { customAlphabet, nanoid } from "nanoid";
|
||||
import nodeCrypto from "node:crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type Triple } from "@recall/types";
|
||||
import { type Triple } from "@core/types";
|
||||
import { type CoreMessage } from "ai";
|
||||
|
||||
/**
|
||||
|
||||
@ -2,6 +2,7 @@ import { redirect } from "@remix-run/node";
|
||||
import { getUserById } from "~/models/user.server";
|
||||
import { sessionStorage } from "./sessionStorage.server";
|
||||
import { getImpersonationId } from "./impersonation.server";
|
||||
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||
|
||||
export async function getUserId(request: Request): Promise<string | undefined> {
|
||||
const impersonatedUserId = await getImpersonationId(request);
|
||||
@ -24,6 +25,46 @@ export async function getUser(request: Request) {
|
||||
throw await logout(request);
|
||||
}
|
||||
|
||||
export async function requireUserId(request: Request, redirectTo?: string) {
|
||||
const userId = await getUserId(request);
|
||||
if (!userId) {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = new URLSearchParams([
|
||||
["redirectTo", redirectTo ?? `${url.pathname}${url.search}`],
|
||||
]);
|
||||
throw redirect(`/login?${searchParams}`);
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
export async function requireUser(request: Request) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const impersonationId = await getImpersonationId(request);
|
||||
const user = await getUserById(userId);
|
||||
if (user) {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
admin: user.admin,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
confirmedBasicDetails: user.confirmedBasicDetails,
|
||||
isImpersonating: !!impersonationId,
|
||||
};
|
||||
}
|
||||
|
||||
throw await logout(request);
|
||||
}
|
||||
|
||||
export async function requireWorkpace(request: Request) {
|
||||
const userId = await requireUserId(request);
|
||||
return getWorkspaceByUser(userId);
|
||||
}
|
||||
|
||||
export async function logout(request: Request) {
|
||||
return redirect("/logout");
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(91.28% 0 0);
|
||||
@ -30,6 +31,14 @@
|
||||
--input: oklch(0% 0 0 / 6.27%);
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 8px;
|
||||
--sidebar: hsl(0 0% 98%);
|
||||
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
||||
--sidebar-primary: hsl(240 5.9% 10%);
|
||||
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||
--sidebar-accent: hsl(240 4.8% 95.9%);
|
||||
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
||||
--sidebar-border: hsl(220 13% 91%);
|
||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||
}
|
||||
|
||||
.dark,
|
||||
@ -60,7 +69,261 @@
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
}
|
||||
|
||||
:root {
|
||||
--gray-50: #f9f9f9;
|
||||
--gray-100: #efefef;
|
||||
--gray-200: #e8e8e8;
|
||||
--gray-300: #e0e0e0;
|
||||
--gray-400: #cecece;
|
||||
--gray-500: #d8d8d8;
|
||||
--gray-600: #bbbbbb;
|
||||
--gray-700: #8d8d8d;
|
||||
--gray-800: #838383;
|
||||
--gray-900: #646464;
|
||||
--gray-950: #202020;
|
||||
|
||||
--grayAlpha-50: 0% 0 0 / 2.35%;
|
||||
--grayAlpha-100: 0% 0 0 / 6.27%;
|
||||
--grayAlpha-200: 0% 0 0 / 9.02%;
|
||||
--grayAlpha-300: 0% 0 0 / 12.16%;
|
||||
--grayAlpha-400: 0% 0 0 / 15.29%;
|
||||
--grayAlpha-500: 0% 0 0 / 19.22%;
|
||||
--grayAlpha-600: 0% 0 0 / 26.67%;
|
||||
--grayAlpha-700: 0% 0 0 / 44.71%;
|
||||
--grayAlpha-800: 0% 0 0 / 48.63%;
|
||||
--grayAlpha-900: 0% 0 0 / 60.78%;
|
||||
--grayAlpha-950: 0% 0 0 / 87.45%;
|
||||
|
||||
/* Colors for different status */
|
||||
--status-pill-0: #d94b0e26;
|
||||
--status-icon-0: #d94b0e;
|
||||
|
||||
--status-pill-1: #9f3def26;
|
||||
--status-icon-1: #9f3def;
|
||||
|
||||
--status-pill-2: #8e862c26;
|
||||
--status-icon-2: #8e862c;
|
||||
|
||||
--status-pill-3: #5c5c5c26;
|
||||
--status-icon-3: #5c5c5c;
|
||||
|
||||
--status-pill-4: #c28c1126;
|
||||
--status-icon-4: #c28c11;
|
||||
|
||||
--status-pill-5: #3f8ef726;
|
||||
--status-icon-5: #3f8ef7;
|
||||
|
||||
--status-pill-6: #3caf2026;
|
||||
--status-icon-6: #3caf20;
|
||||
|
||||
/* Avatar colors */
|
||||
--custom-color-1: #b56455;
|
||||
--custom-color-2: #7b8a34;
|
||||
--custom-color-3: #1c91a8;
|
||||
--custom-color-4: #886dbc;
|
||||
--custom-color-5: #ad6e30;
|
||||
--custom-color-6: #54935b;
|
||||
--custom-color-7: #4187c0;
|
||||
--custom-color-8: #a165a1;
|
||||
--custom-color-9: #997d1d;
|
||||
--custom-color-10: #2b9684;
|
||||
--custom-color-11: #2b9684;
|
||||
--custom-color-12: #b0617c;
|
||||
|
||||
/* Team Colors */
|
||||
--team-color-1: #89c794;
|
||||
--team-color-2: #d2a1bb;
|
||||
--team-color-3: #90c5d6;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--gray-50: #191919;
|
||||
--gray-100: #222222;
|
||||
--gray-200: #2a2a2a;
|
||||
--gray-300: #313131;
|
||||
--gray-400: #3a3a3a;
|
||||
--gray-500: #484848;
|
||||
--gray-600: #606060;
|
||||
--gray-700: #6e6e6e;
|
||||
--gray-800: #7b7b7b;
|
||||
--gray-900: #b4b4b4;
|
||||
--gray-950: #eeeeee;
|
||||
|
||||
--grayAlpha-50: 100% 0 0 / 3.53%;
|
||||
--grayAlpha-100: 100% 0 0 / 7.06%;
|
||||
--grayAlpha-200: 100% 0 0 / 10.59%;
|
||||
--grayAlpha-300: 100% 0 0 / 13.33%;
|
||||
--grayAlpha-400: 100% 0 0 / 17.25%;
|
||||
--grayAlpha-500: 100% 0 0 / 23.14%;
|
||||
--grayAlpha-600: 100% 0 0 / 33.33%;
|
||||
--grayAlpha-700: 100% 0 0 / 39.22%;
|
||||
--grayAlpha-800: 100% 0 0 / 44.31%;
|
||||
--grayAlpha-900: 100% 0 0 / 68.63%;
|
||||
--grayAlpha-950: 100% 0 0 / 92.94%;
|
||||
|
||||
/* Colors for different status */
|
||||
--status-pill-0: #c06142;
|
||||
--status-icon-0: #c06142;
|
||||
|
||||
--status-pill-1: #886dbc;
|
||||
--status-icon-1: #886dbc;
|
||||
|
||||
--status-pill-2: #83872c;
|
||||
--status-icon-2: #83872c;
|
||||
|
||||
--status-pill-3: #b4b4b4;
|
||||
--status-icon-3: #5c5c5c;
|
||||
|
||||
--status-pill-4: #c28c11;
|
||||
--status-icon-4: #c28c11;
|
||||
|
||||
--status-pill-5: #4187c0;
|
||||
--status-icon-5: #4187c0;
|
||||
|
||||
--status-pill-6: #5d9151;
|
||||
--status-icon-6: #5d9151;
|
||||
|
||||
/* Team Colors */
|
||||
--team-color-1: #54935b;
|
||||
--team-color-2: #a165a1;
|
||||
--team-color-3: #4187c0;
|
||||
--sidebar: hsl(240 5.9% 10%);
|
||||
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
||||
--sidebar-primary: hsl(224.3 76.3% 48%);
|
||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||
--sidebar-accent: hsl(240 3.7% 15.9%);
|
||||
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||
--sidebar-border: hsl(240 3.7% 15.9%);
|
||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
--color-border: var(--border);
|
||||
--color-border-dark: var(--border-dark);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-background-2: var(--background-2);
|
||||
--color-background-3: var(--background-3);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-foreground: var(--warning-foreground);
|
||||
--color-success: var(--success);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
|
||||
--color-gray-50: var(--gray-50);
|
||||
--color-gray-100: var(--gray-100);
|
||||
--color-gray-200: var(--gray-200);
|
||||
--color-gray-300: var(--gray-300);
|
||||
--color-gray-400: var(--gray-400);
|
||||
--color-gray-500: var(--gray-500);
|
||||
--color-gray-600: var(--gray-600);
|
||||
--color-gray-700: var(--gray-700);
|
||||
--color-gray-800: var(--gray-800);
|
||||
--color-gray-900: var(--gray-900);
|
||||
--color-gray-950: var(--gray-950);
|
||||
|
||||
--color-grayAlpha-50: oklch(var(--grayAlpha-50));
|
||||
--color-grayAlpha-100: oklch(var(--grayAlpha-100));
|
||||
--color-grayAlpha-200: oklch(var(--grayAlpha-200));
|
||||
--color-grayAlpha-300: oklch(var(--grayAlpha-300));
|
||||
--color-grayAlpha-400: oklch(var(--grayAlpha-400));
|
||||
--color-grayAlpha-500: oklch(var(--grayAlpha-500));
|
||||
--color-grayAlpha-600: oklch(var(--grayAlpha-600));
|
||||
--color-grayAlpha-700: oklch(var(--grayAlpha-700));
|
||||
--color-grayAlpha-800: oklch(var(--grayAlpha-800));
|
||||
--color-grayAlpha-900: oklch(var(--grayAlpha-900));
|
||||
--color-grayAlpha-950: oklch(var(--grayAlpha-950));
|
||||
|
||||
--color-red-50: #fdf3f3;
|
||||
--color-red-100: #fbe9e8;
|
||||
--color-red-200: #f7d4d4;
|
||||
--color-red-300: #f0b1b1;
|
||||
--color-red-400: #e78587;
|
||||
--color-red-500: #d75056;
|
||||
--color-red-600: #c43a46;
|
||||
--color-red-700: #a52b3a;
|
||||
--color-red-800: #8a2735;
|
||||
--color-red-900: #772433;
|
||||
--color-red-950: #420f18;
|
||||
|
||||
--color-orange-50: #fdf6ef;
|
||||
--color-orange-100: #fbead9;
|
||||
--color-orange-200: #f7d2b1;
|
||||
--color-orange-300: #f1b480;
|
||||
--color-orange-400: #ea8c4d;
|
||||
--color-orange-500: #e67333;
|
||||
--color-orange-600: #d65520;
|
||||
--color-orange-700: #b2401c;
|
||||
--color-orange-800: #8e341e;
|
||||
--color-orange-900: #732d1b;
|
||||
--color-orange-950: #3e140c;
|
||||
|
||||
--color-yellow-50: #fdfbe9;
|
||||
--color-yellow-100: #faf7c7;
|
||||
--color-yellow-200: #f7ec91;
|
||||
--color-yellow-300: #f1db53;
|
||||
--color-yellow-400: #ebc724;
|
||||
--color-yellow-500: #dcb016;
|
||||
--color-yellow-600: #c28c11;
|
||||
--color-yellow-700: #976211;
|
||||
--color-yellow-800: #7d4f16;
|
||||
--color-yellow-900: #6b4118;
|
||||
--color-yellow-950: #3e220a;
|
||||
|
||||
--text-xs: 12px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 14px;
|
||||
--text-md: 15px;
|
||||
--text-lg: 17px;
|
||||
--text-xl: 22px;
|
||||
--text-2xl: 26px;
|
||||
|
||||
--radius-none: 0px;
|
||||
--radius: 0.375rem;
|
||||
--radius-sm: 0.125rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
--radius-2xl: 1rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
--radius-4xl: 2rem;
|
||||
--radius-5xl: 3rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
--shadow: 0px 6px 20px 0px rgba(0, 0, 0, 0.15), 0px 0px 2px 0px rgba(0, 0, 0, 0.2);
|
||||
--shadow-1: 0px 6px 20px 0px rgba(0, 0, 0, 0.15), 0px 0px 2px 0px rgba(0, 0, 0, 0.2);
|
||||
|
||||
--font-sans: "Geist Variable", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||
--font-mono: "Geist Mono Variable", monaco, Consolas, "Lucida Console", monospace;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
7
apps/webapp/app/utils/pathBuilder.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function confirmBasicDetailsPath() {
|
||||
return `/confirm-basic-details`;
|
||||
}
|
||||
|
||||
export function rootPath() {
|
||||
return `/`;
|
||||
}
|
||||
100
apps/webapp/app/utils/presets/apps.ts
Normal file
@ -0,0 +1,100 @@
|
||||
export enum Apps {
|
||||
LINEAR = "LINEAR",
|
||||
SLACK = "SLACK",
|
||||
}
|
||||
|
||||
export const AppNames = {
|
||||
[Apps.LINEAR]: "Linear",
|
||||
[Apps.SLACK]: "Slack",
|
||||
} as const;
|
||||
|
||||
// General node types that are common across all apps
|
||||
export const GENERAL_NODE_TYPES = {
|
||||
PERSON: {
|
||||
name: "Person",
|
||||
description: "Represents an individual, like a team member or contact",
|
||||
},
|
||||
APP: {
|
||||
name: "App",
|
||||
description: "A software application or service that's integrated",
|
||||
},
|
||||
PLACE: {
|
||||
name: "Place",
|
||||
description: "A physical location like an office, meeting room, or city",
|
||||
},
|
||||
ORGANIZATION: {
|
||||
name: "Organization",
|
||||
description: "A company, team, or any formal group of people",
|
||||
},
|
||||
EVENT: {
|
||||
name: "Event",
|
||||
description: "A meeting, deadline, or any time-based occurrence",
|
||||
},
|
||||
} as const;
|
||||
|
||||
// App-specific node types
|
||||
export const APP_NODE_TYPES = {
|
||||
[Apps.LINEAR]: {
|
||||
ISSUE: {
|
||||
name: "Linear Issue",
|
||||
description: "A task, bug report, or feature request tracked in Linear",
|
||||
},
|
||||
PROJECT: {
|
||||
name: "Linear Project",
|
||||
description: "A collection of related issues and work items in Linear",
|
||||
},
|
||||
CYCLE: {
|
||||
name: "Linear Cycle",
|
||||
description: "A time-boxed iteration of work in Linear",
|
||||
},
|
||||
TEAM: {
|
||||
name: "Linear Team",
|
||||
description: "A group of people working together in Linear",
|
||||
},
|
||||
LABEL: {
|
||||
name: "Linear Label",
|
||||
description: "A tag used to categorize and organize issues in Linear",
|
||||
},
|
||||
},
|
||||
[Apps.SLACK]: {
|
||||
CHANNEL: {
|
||||
name: "Slack Channel",
|
||||
description: "A dedicated space for team communication in Slack",
|
||||
},
|
||||
THREAD: {
|
||||
name: "Slack Thread",
|
||||
description: "A focused conversation branch within a Slack channel",
|
||||
},
|
||||
MESSAGE: {
|
||||
name: "Slack Message",
|
||||
description: "A single communication sent in a Slack channel or thread",
|
||||
},
|
||||
REACTION: {
|
||||
name: "Slack Reaction",
|
||||
description: "An emoji response to a message in Slack",
|
||||
},
|
||||
FILE: {
|
||||
name: "Slack File",
|
||||
description: "A document, image or other file shared in Slack",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Returns both general node types and app-specific node types for given apps
|
||||
* @param apps Array of app names to get node types for
|
||||
* @returns Object containing general and app-specific node types
|
||||
*/
|
||||
export function getNodeTypes(apps: Array<keyof typeof APP_NODE_TYPES>) {
|
||||
const appSpecificTypes = apps.reduce((acc, appName) => {
|
||||
return {
|
||||
...acc,
|
||||
[appName]: APP_NODE_TYPES[appName],
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
general: GENERAL_NODE_TYPES,
|
||||
appSpecific: appSpecificTypes,
|
||||
};
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
export function singleton<T>(name: string, getValue: () => T): T {
|
||||
const thusly = globalThis as any;
|
||||
thusly.__recall_singletons ??= {};
|
||||
thusly.__recall_singletons[name] ??= getValue();
|
||||
return thusly.__recall_singletons[name];
|
||||
thusly.__core_singletons ??= {};
|
||||
thusly.__core_singletons[name] ??= getValue();
|
||||
return thusly.__core_singletons[name];
|
||||
}
|
||||
|
||||
@ -13,10 +13,29 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^1.3.21",
|
||||
"@coji/remix-auth-google": "^4.2.0",
|
||||
"@conform-to/react": "^0.6.1",
|
||||
"@conform-to/zod": "^0.6.1",
|
||||
"@core/database": "workspace:*",
|
||||
"@core/types": "workspace:*",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@recall/database": "workspace:*",
|
||||
"@recall/types": "workspace:*",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@remix-run/express": "2.16.7",
|
||||
"@remix-run/node": "2.1.0",
|
||||
"@remix-run/react": "2.16.7",
|
||||
@ -33,6 +52,9 @@
|
||||
"clsx": "^2.1.1",
|
||||
"compression": "^1.7.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"d3": "^7.9.0",
|
||||
"dayjs": "^1.11.10",
|
||||
|
||||
"express": "^4.18.1",
|
||||
"ioredis": "^5.6.1",
|
||||
"isbot": "^4.1.0",
|
||||
@ -67,6 +89,7 @@
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/react": "^18.2.20",
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 7.8 KiB |
12
apps/webapp/public/logo-dark.svg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 59 KiB |
12
apps/webapp/public/logo-light.svg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
@ -27,134 +27,6 @@ export default {
|
||||
"monospace",
|
||||
],
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
colors: {
|
||||
border: {
|
||||
DEFAULT: "oklch(var(--border))",
|
||||
dark: "oklch(var(--border-dark))",
|
||||
},
|
||||
input: "oklch(var(--input))",
|
||||
ring: "oklch(var(--ring))",
|
||||
background: {
|
||||
2: "oklch(var(--background-2) / <alpha-value>)",
|
||||
3: "oklch(var(--background-3) / <alpha-value>)",
|
||||
DEFAULT: "oklch(var(--background) / <alpha-value>)",
|
||||
},
|
||||
foreground: "oklch(var(--foreground) / <alpha-value>)",
|
||||
primary: {
|
||||
DEFAULT: "oklch(var(--primary) / <alpha-value>)",
|
||||
foreground: "oklch(var(--primary-foreground) / <alpha-value>)",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "oklch(var(--destructive) / <alpha-value>)",
|
||||
foreground: "oklch(var(--destructive-foreground) / <alpha-value>)",
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: "oklch(var(--warning) / <alpha-value>)",
|
||||
foreground: "oklch(var(--warning-foreground) / <alpha-value>)",
|
||||
},
|
||||
success: {
|
||||
DEFAULT: "oklch(var(--success) / <alpha-value>)",
|
||||
foreground: "oklch(var(--success-foreground) / <alpha-value>)",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "oklch(var(--muted))",
|
||||
foreground: "oklch(var(--muted-foreground) / <alpha-value>)",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "oklch(var(--accent) / <alpha-value>)",
|
||||
foreground: "oklch(var(--accent-foreground) / <alpha-value>)",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "oklch(var(--popover) / <alpha-value>)",
|
||||
foreground: "oklch(var(--popover-foreground) / <alpha-value>)",
|
||||
},
|
||||
gray: {
|
||||
50: "var(--gray-50)",
|
||||
100: "var(--gray-100)",
|
||||
200: "var(--gray-200)",
|
||||
300: "var(--gray-300)",
|
||||
400: "var(--gray-400)",
|
||||
500: "var(--gray-500)",
|
||||
600: "var(--gray-600)",
|
||||
700: "var(--gray-700)",
|
||||
800: "var(--gray-800)",
|
||||
900: "var(--gray-900)",
|
||||
950: "var(--gray-950)",
|
||||
},
|
||||
grayAlpha: {
|
||||
50: "oklch(var(--grayAlpha-50))",
|
||||
100: "oklch(var(--grayAlpha-100))",
|
||||
200: "oklch(var(--grayAlpha-200))",
|
||||
300: "oklch(var(--grayAlpha-300))",
|
||||
400: "oklch(var(--grayAlpha-400))",
|
||||
500: "oklch(var(--grayAlpha-500))",
|
||||
600: "oklch(var(--grayAlpha-600))",
|
||||
700: "oklch(var(--grayAlpha-700))",
|
||||
800: "oklch(var(--grayAlpha-800))",
|
||||
900: "oklch(var(--grayAlpha-900))",
|
||||
950: "oklch(var(--grayAlpha-950))",
|
||||
},
|
||||
red: {
|
||||
50: "#fdf3f3",
|
||||
100: "#fbe9e8",
|
||||
200: "#f7d4d4",
|
||||
300: "#f0b1b1",
|
||||
400: "#e78587",
|
||||
500: "#d75056",
|
||||
600: "#c43a46",
|
||||
700: "#a52b3a",
|
||||
800: "#8a2735",
|
||||
900: "#772433",
|
||||
950: "#420f18",
|
||||
},
|
||||
orange: {
|
||||
50: "#fdf6ef",
|
||||
100: "#fbead9",
|
||||
200: "#f7d2b1",
|
||||
300: "#f1b480",
|
||||
400: "#ea8c4d",
|
||||
500: "#e67333",
|
||||
600: "#d65520",
|
||||
700: "#b2401c",
|
||||
800: "#8e341e",
|
||||
900: "#732d1b",
|
||||
950: "#3e140c",
|
||||
},
|
||||
yellow: {
|
||||
50: "#fdfbe9",
|
||||
100: "#faf7c7",
|
||||
200: "#f7ec91",
|
||||
300: "#f1db53",
|
||||
400: "#ebc724",
|
||||
500: "#dcb016",
|
||||
600: "#c28c11",
|
||||
700: "#976211",
|
||||
800: "#7d4f16",
|
||||
900: "#6b4118",
|
||||
950: "#3e220a",
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: "oklch(var(--background-2) / <alpha-value>)",
|
||||
foreground: "oklch(var(--foreground) / <alpha-value>)",
|
||||
primary: "oklch(var(--primary) / <alpha-value>)",
|
||||
"primary-foreground":
|
||||
"oklch(var(--primary-foreground) / <alpha-value>)",
|
||||
accent: "oklch(var(--accent) / <alpha-value>)",
|
||||
"accent-foreground":
|
||||
"oklch(var(--accent-foreground) / <alpha-value>)",
|
||||
border: "oklch(var(--border))",
|
||||
ring: "oklch(var(--ring))",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "recall",
|
||||
"name": "core",
|
||||
"private": true,
|
||||
"workspaces":
|
||||
[ "apps/*", "packages/*" ]
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@recall/database",
|
||||
"name": "@core/database",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Workspace" ADD COLUMN "integrations" TEXT[];
|
||||
@ -52,6 +52,8 @@ model Workspace {
|
||||
slug String @unique
|
||||
icon String?
|
||||
|
||||
integrations String[]
|
||||
|
||||
userId String? @unique
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@recall/types",
|
||||
"name": "@core/types",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"main": "./dist/index.js",
|
||||
|
||||