calabasas 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +656 -128
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2594,7 +2594,7 @@ async function generate(options) {
|
|
|
2594
2594
|
import * as fs5 from "fs";
|
|
2595
2595
|
import * as path5 from "path";
|
|
2596
2596
|
import * as p6 from "@clack/prompts";
|
|
2597
|
-
var
|
|
2597
|
+
var SKILL_PATH = ".claude/skills/calabasas/SKILL.md";
|
|
2598
2598
|
async function fetchSkillContent(env) {
|
|
2599
2599
|
const apiUrl = getApiUrlForEnv(env);
|
|
2600
2600
|
const response = await fetch(`${apiUrl}/api/skill`);
|
|
@@ -2603,28 +2603,6 @@ async function fetchSkillContent(env) {
|
|
|
2603
2603
|
}
|
|
2604
2604
|
return response.text();
|
|
2605
2605
|
}
|
|
2606
|
-
var SEARCH_PATHS = [
|
|
2607
|
-
"CLAUDE.md",
|
|
2608
|
-
"AGENTS.md",
|
|
2609
|
-
".claude/CLAUDE.md",
|
|
2610
|
-
".cursor/AGENTS.md",
|
|
2611
|
-
"docs/CLAUDE.md",
|
|
2612
|
-
"docs/AGENTS.md"
|
|
2613
|
-
];
|
|
2614
|
-
function detectExistingFiles(cwd) {
|
|
2615
|
-
const found = [];
|
|
2616
|
-
for (const searchPath of SEARCH_PATHS) {
|
|
2617
|
-
const fullPath = path5.resolve(cwd, searchPath);
|
|
2618
|
-
if (fs5.existsSync(fullPath)) {
|
|
2619
|
-
const type = searchPath.includes("CLAUDE") ? "CLAUDE.md" : "AGENTS.md";
|
|
2620
|
-
found.push({ path: searchPath, fullPath, type });
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
return found;
|
|
2624
|
-
}
|
|
2625
|
-
function hasCalabsasSection(content) {
|
|
2626
|
-
return content.includes(SECTION_HEADER) || content.includes("## Calabasas Guidelines");
|
|
2627
|
-
}
|
|
2628
2606
|
async function skill(options) {
|
|
2629
2607
|
const env = resolveEnv(options);
|
|
2630
2608
|
p6.intro("calabasas skill");
|
|
@@ -2640,110 +2618,13 @@ async function skill(options) {
|
|
|
2640
2618
|
}
|
|
2641
2619
|
s.stop("Guidelines fetched");
|
|
2642
2620
|
const cwd = process.cwd();
|
|
2643
|
-
const
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
createPath = options.file;
|
|
2648
|
-
} else {
|
|
2649
|
-
const selected = await p6.select({
|
|
2650
|
-
message: "No CLAUDE.md or AGENTS.md found. Where should we create one?",
|
|
2651
|
-
options: [
|
|
2652
|
-
{ value: "CLAUDE.md", label: "CLAUDE.md (project root)" },
|
|
2653
|
-
{ value: "AGENTS.md", label: "AGENTS.md (project root)" },
|
|
2654
|
-
{ value: ".claude/CLAUDE.md", label: ".claude/CLAUDE.md" }
|
|
2655
|
-
]
|
|
2656
|
-
});
|
|
2657
|
-
if (p6.isCancel(selected)) {
|
|
2658
|
-
p6.cancel("Cancelled.");
|
|
2659
|
-
return;
|
|
2660
|
-
}
|
|
2661
|
-
createPath = selected;
|
|
2662
|
-
}
|
|
2663
|
-
const fullPath = path5.resolve(cwd, createPath);
|
|
2664
|
-
const dir = path5.dirname(fullPath);
|
|
2665
|
-
if (!fs5.existsSync(dir)) {
|
|
2666
|
-
fs5.mkdirSync(dir, { recursive: true });
|
|
2667
|
-
}
|
|
2668
|
-
const projectName = path5.basename(cwd);
|
|
2669
|
-
const initialContent = `# ${projectName}
|
|
2670
|
-
|
|
2671
|
-
${skillContent}`;
|
|
2672
|
-
fs5.writeFileSync(fullPath, initialContent);
|
|
2673
|
-
p6.outro(`Created ${createPath} with Calabasas guidelines`);
|
|
2674
|
-
return;
|
|
2675
|
-
}
|
|
2676
|
-
let selectedFile;
|
|
2677
|
-
if (options.file) {
|
|
2678
|
-
const match = detectedFiles.find((f) => f.path === options.file);
|
|
2679
|
-
if (match) {
|
|
2680
|
-
selectedFile = match;
|
|
2681
|
-
} else {
|
|
2682
|
-
const fullPath = path5.resolve(cwd, options.file);
|
|
2683
|
-
const dir = path5.dirname(fullPath);
|
|
2684
|
-
if (!fs5.existsSync(dir)) {
|
|
2685
|
-
fs5.mkdirSync(dir, { recursive: true });
|
|
2686
|
-
}
|
|
2687
|
-
if (fs5.existsSync(fullPath)) {
|
|
2688
|
-
selectedFile = {
|
|
2689
|
-
path: options.file,
|
|
2690
|
-
fullPath,
|
|
2691
|
-
type: options.file.includes("CLAUDE") ? "CLAUDE.md" : "AGENTS.md"
|
|
2692
|
-
};
|
|
2693
|
-
} else {
|
|
2694
|
-
const projectName = path5.basename(cwd);
|
|
2695
|
-
const initialContent = `# ${projectName}
|
|
2696
|
-
|
|
2697
|
-
${skillContent}`;
|
|
2698
|
-
fs5.writeFileSync(fullPath, initialContent);
|
|
2699
|
-
p6.outro(`Created ${options.file} with Calabasas guidelines`);
|
|
2700
|
-
return;
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
} else if (detectedFiles.length === 1) {
|
|
2704
|
-
selectedFile = detectedFiles[0];
|
|
2705
|
-
} else {
|
|
2706
|
-
const selected = await p6.select({
|
|
2707
|
-
message: "Which file should receive the Calabasas guidelines?",
|
|
2708
|
-
options: detectedFiles.map((file) => ({
|
|
2709
|
-
value: file.path,
|
|
2710
|
-
label: file.path
|
|
2711
|
-
}))
|
|
2712
|
-
});
|
|
2713
|
-
if (p6.isCancel(selected)) {
|
|
2714
|
-
p6.cancel("Cancelled.");
|
|
2715
|
-
return;
|
|
2716
|
-
}
|
|
2717
|
-
selectedFile = detectedFiles.find((f) => f.path === selected);
|
|
2718
|
-
}
|
|
2719
|
-
const existingContent = fs5.readFileSync(selectedFile.fullPath, "utf8");
|
|
2720
|
-
if (hasCalabsasSection(existingContent)) {
|
|
2721
|
-
if (!options.yes) {
|
|
2722
|
-
const update = await p6.confirm({
|
|
2723
|
-
message: `Calabasas guidelines already exist in ${selectedFile.path}. Replace?`
|
|
2724
|
-
});
|
|
2725
|
-
if (p6.isCancel(update) || !update) {
|
|
2726
|
-
p6.cancel("Cancelled.");
|
|
2727
|
-
return;
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
const sectionRegex = /## Calabasas Guidelines[\s\S]*?(?=\n## |\n# |$)/;
|
|
2731
|
-
const contentWithoutSection = existingContent.replace(sectionRegex, "").trimEnd();
|
|
2732
|
-
const newContent = `${contentWithoutSection}
|
|
2733
|
-
|
|
2734
|
-
${skillContent}`;
|
|
2735
|
-
fs5.writeFileSync(selectedFile.fullPath, newContent);
|
|
2736
|
-
p6.outro(`Updated Calabasas guidelines in ${selectedFile.path}`);
|
|
2737
|
-
} else {
|
|
2738
|
-
const separator = existingContent.endsWith(`
|
|
2739
|
-
`) ? `
|
|
2740
|
-
` : `
|
|
2741
|
-
|
|
2742
|
-
`;
|
|
2743
|
-
const newContent = `${existingContent}${separator}${skillContent}`;
|
|
2744
|
-
fs5.writeFileSync(selectedFile.fullPath, newContent);
|
|
2745
|
-
p6.outro(`Added Calabasas guidelines to ${selectedFile.path}`);
|
|
2621
|
+
const fullPath = path5.resolve(cwd, SKILL_PATH);
|
|
2622
|
+
const dir = path5.dirname(fullPath);
|
|
2623
|
+
if (!fs5.existsSync(dir)) {
|
|
2624
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
2746
2625
|
}
|
|
2626
|
+
fs5.writeFileSync(fullPath, skillContent);
|
|
2627
|
+
p6.outro(`Wrote Calabasas skill to ${SKILL_PATH}`);
|
|
2747
2628
|
}
|
|
2748
2629
|
|
|
2749
2630
|
// src/commands/add.ts
|
|
@@ -5741,6 +5622,652 @@ export const listAppEmojis = query({
|
|
|
5741
5622
|
});`
|
|
5742
5623
|
};
|
|
5743
5624
|
|
|
5625
|
+
// src/lib/registry/components/role-creator.ts
|
|
5626
|
+
var roleCreator = {
|
|
5627
|
+
name: "role-creator",
|
|
5628
|
+
kind: "component",
|
|
5629
|
+
description: "Discord-style role creation dialog with color picker, member assignment, and position ordering",
|
|
5630
|
+
requiredSyncTypes: ["roles", "members"],
|
|
5631
|
+
requiredShadcnComponents: ["dialog", "button", "popover", "command"],
|
|
5632
|
+
generateReactComponent: () => `"use client";
|
|
5633
|
+
|
|
5634
|
+
import { useState, useMemo } from "react";
|
|
5635
|
+
import { useQuery } from "convex/react";
|
|
5636
|
+
import { api } from "@/convex/_generated/api";
|
|
5637
|
+
import {
|
|
5638
|
+
Dialog,
|
|
5639
|
+
DialogContent,
|
|
5640
|
+
DialogDescription,
|
|
5641
|
+
DialogHeader,
|
|
5642
|
+
DialogTitle,
|
|
5643
|
+
DialogTrigger,
|
|
5644
|
+
} from "@/components/ui/dialog";
|
|
5645
|
+
import {
|
|
5646
|
+
Popover,
|
|
5647
|
+
PopoverContent,
|
|
5648
|
+
PopoverTrigger,
|
|
5649
|
+
} from "@/components/ui/popover";
|
|
5650
|
+
import {
|
|
5651
|
+
Command,
|
|
5652
|
+
CommandEmpty,
|
|
5653
|
+
CommandGroup,
|
|
5654
|
+
CommandInput,
|
|
5655
|
+
CommandItem,
|
|
5656
|
+
CommandList,
|
|
5657
|
+
} from "@/components/ui/command";
|
|
5658
|
+
import { Button } from "@/components/ui/button";
|
|
5659
|
+
import { cn } from "@/lib/utils";
|
|
5660
|
+
import {
|
|
5661
|
+
Check,
|
|
5662
|
+
X,
|
|
5663
|
+
ChevronsUpDown,
|
|
5664
|
+
User,
|
|
5665
|
+
Plus,
|
|
5666
|
+
ChevronUp,
|
|
5667
|
+
ChevronDown,
|
|
5668
|
+
} from "lucide-react";
|
|
5669
|
+
|
|
5670
|
+
/**
|
|
5671
|
+
* Discord's 20 preset role colors arranged in two rows:
|
|
5672
|
+
* Row 1 — lighter tones, Row 2 — darker counterparts.
|
|
5673
|
+
*/
|
|
5674
|
+
const PRESET_COLORS: number[] = [
|
|
5675
|
+
0x1abc9c, 0x2ecc71, 0x3498db, 0x9b59b6, 0xe91e63,
|
|
5676
|
+
0xf1c40f, 0xe67e22, 0xe74c3c, 0x95a5a6, 0x607d8b,
|
|
5677
|
+
0x11806a, 0x1f8b4c, 0x206694, 0x71368a, 0xad1457,
|
|
5678
|
+
0xc27c0e, 0xa84300, 0x992d22, 0x979c9f, 0x546e7a,
|
|
5679
|
+
];
|
|
5680
|
+
|
|
5681
|
+
/** Convert Discord integer color to hex CSS string */
|
|
5682
|
+
function colorToHex(color: number): string {
|
|
5683
|
+
if (color === 0) return "#99AAB5";
|
|
5684
|
+
return "#" + color.toString(16).padStart(6, "0");
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5687
|
+
/** Convert hex string to Discord integer color */
|
|
5688
|
+
function hexToColor(hex: string): number {
|
|
5689
|
+
const clean = hex.replace("#", "");
|
|
5690
|
+
const parsed = parseInt(clean, 16);
|
|
5691
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
5692
|
+
}
|
|
5693
|
+
|
|
5694
|
+
/** Convert Discord integer color to rgba string */
|
|
5695
|
+
function colorToRgba(color: number, alpha: number): string {
|
|
5696
|
+
if (color === 0) return \`rgba(153, 170, 181, \${alpha})\`;
|
|
5697
|
+
const r = (color >> 16) & 0xff;
|
|
5698
|
+
const g = (color >> 8) & 0xff;
|
|
5699
|
+
const b = color & 0xff;
|
|
5700
|
+
return \`rgba(\${r}, \${g}, \${b}, \${alpha})\`;
|
|
5701
|
+
}
|
|
5702
|
+
|
|
5703
|
+
/** Build Discord CDN avatar URL */
|
|
5704
|
+
function avatarUrl(
|
|
5705
|
+
userId: string,
|
|
5706
|
+
avatar: string | undefined,
|
|
5707
|
+
): string | null {
|
|
5708
|
+
if (!avatar) return null;
|
|
5709
|
+
return \`https://cdn.discordapp.com/avatars/\${userId}/\${avatar}.png?size=32\`;
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
5713
|
+
|
|
5714
|
+
type RoleCreatorData = {
|
|
5715
|
+
name: string;
|
|
5716
|
+
color: number;
|
|
5717
|
+
memberIds: string[];
|
|
5718
|
+
position: number;
|
|
5719
|
+
};
|
|
5720
|
+
|
|
5721
|
+
type RoleCreatorProps = {
|
|
5722
|
+
guildDiscordId: string;
|
|
5723
|
+
defaultMemberIds?: string[];
|
|
5724
|
+
open?: boolean;
|
|
5725
|
+
onOpenChange?: (open: boolean) => void;
|
|
5726
|
+
onCreate?: (data: RoleCreatorData) => void;
|
|
5727
|
+
trigger?: React.ReactNode;
|
|
5728
|
+
className?: string;
|
|
5729
|
+
};
|
|
5730
|
+
|
|
5731
|
+
// ── Component ────────────────────────────────────────────────────────────────
|
|
5732
|
+
|
|
5733
|
+
export function RoleCreator({
|
|
5734
|
+
guildDiscordId,
|
|
5735
|
+
defaultMemberIds = [],
|
|
5736
|
+
open: controlledOpen,
|
|
5737
|
+
onOpenChange,
|
|
5738
|
+
onCreate,
|
|
5739
|
+
trigger,
|
|
5740
|
+
className,
|
|
5741
|
+
}: RoleCreatorProps) {
|
|
5742
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
5743
|
+
const isControlled = controlledOpen !== undefined;
|
|
5744
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
5745
|
+
|
|
5746
|
+
// Form state
|
|
5747
|
+
const [name, setName] = useState("new role");
|
|
5748
|
+
const [color, setColor] = useState(0);
|
|
5749
|
+
const [customHex, setCustomHex] = useState("");
|
|
5750
|
+
const [usingCustom, setUsingCustom] = useState(false);
|
|
5751
|
+
const [selectedMembers, setSelectedMembers] = useState<string[]>(defaultMemberIds);
|
|
5752
|
+
const [memberPickerOpen, setMemberPickerOpen] = useState(false);
|
|
5753
|
+
const [positionIndex, setPositionIndex] = useState(0);
|
|
5754
|
+
|
|
5755
|
+
// Data
|
|
5756
|
+
const roles = useQuery(api.calabasas.queries.listRolesForCreator, {
|
|
5757
|
+
guildDiscordId,
|
|
5758
|
+
});
|
|
5759
|
+
const members = useQuery(api.calabasas.queries.listMembersForCreator, {
|
|
5760
|
+
guildDiscordId,
|
|
5761
|
+
});
|
|
5762
|
+
|
|
5763
|
+
// Sort roles highest-position first, exclude @everyone
|
|
5764
|
+
const sortedRoles = useMemo(() => {
|
|
5765
|
+
if (!roles) return [];
|
|
5766
|
+
return roles
|
|
5767
|
+
.filter((r) => r.name !== "@everyone")
|
|
5768
|
+
.sort((a, b) => b.position - a.position);
|
|
5769
|
+
}, [roles]);
|
|
5770
|
+
|
|
5771
|
+
// Calculate the position value from the insertion index
|
|
5772
|
+
const calculatedPosition = useMemo(() => {
|
|
5773
|
+
if (sortedRoles.length === 0) return 1;
|
|
5774
|
+
if (positionIndex === 0) return sortedRoles[0].position + 1;
|
|
5775
|
+
if (positionIndex >= sortedRoles.length) return 1;
|
|
5776
|
+
return sortedRoles[positionIndex - 1].position;
|
|
5777
|
+
}, [sortedRoles, positionIndex]);
|
|
5778
|
+
|
|
5779
|
+
const reset = () => {
|
|
5780
|
+
setName("new role");
|
|
5781
|
+
setColor(0);
|
|
5782
|
+
setCustomHex("");
|
|
5783
|
+
setUsingCustom(false);
|
|
5784
|
+
setSelectedMembers(defaultMemberIds);
|
|
5785
|
+
setMemberPickerOpen(false);
|
|
5786
|
+
setPositionIndex(0);
|
|
5787
|
+
};
|
|
5788
|
+
|
|
5789
|
+
const setOpen = (v: boolean) => {
|
|
5790
|
+
if (!v) reset();
|
|
5791
|
+
if (!isControlled) setInternalOpen(v);
|
|
5792
|
+
onOpenChange?.(v);
|
|
5793
|
+
};
|
|
5794
|
+
|
|
5795
|
+
const handleCreate = () => {
|
|
5796
|
+
onCreate?.({
|
|
5797
|
+
name,
|
|
5798
|
+
color,
|
|
5799
|
+
memberIds: selectedMembers,
|
|
5800
|
+
position: calculatedPosition,
|
|
5801
|
+
});
|
|
5802
|
+
setOpen(false);
|
|
5803
|
+
};
|
|
5804
|
+
|
|
5805
|
+
const toggleMember = (userId: string) => {
|
|
5806
|
+
setSelectedMembers((prev) =>
|
|
5807
|
+
prev.includes(userId)
|
|
5808
|
+
? prev.filter((id) => id !== userId)
|
|
5809
|
+
: [...prev, userId],
|
|
5810
|
+
);
|
|
5811
|
+
};
|
|
5812
|
+
|
|
5813
|
+
const selectPreset = (c: number) => {
|
|
5814
|
+
setColor(c);
|
|
5815
|
+
setUsingCustom(false);
|
|
5816
|
+
setCustomHex("");
|
|
5817
|
+
};
|
|
5818
|
+
|
|
5819
|
+
const moveUp = () => setPositionIndex((i) => Math.max(0, i - 1));
|
|
5820
|
+
const moveDown = () =>
|
|
5821
|
+
setPositionIndex((i) => Math.min(sortedRoles.length, i + 1));
|
|
5822
|
+
|
|
5823
|
+
// ── Render ───────────────────────────────────────────────────────────────
|
|
5824
|
+
|
|
5825
|
+
return (
|
|
5826
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
5827
|
+
<DialogTrigger asChild>
|
|
5828
|
+
{trigger ?? (
|
|
5829
|
+
<Button variant="outline" className={className}>
|
|
5830
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
5831
|
+
Create Role
|
|
5832
|
+
</Button>
|
|
5833
|
+
)}
|
|
5834
|
+
</DialogTrigger>
|
|
5835
|
+
|
|
5836
|
+
<DialogContent className="sm:max-w-[520px] p-0 gap-0 overflow-hidden">
|
|
5837
|
+
{/* ── Header ──────────────────────────────────────────────── */}
|
|
5838
|
+
<DialogHeader className="px-6 pt-6 pb-2">
|
|
5839
|
+
<DialogTitle>Create Role</DialogTitle>
|
|
5840
|
+
<DialogDescription>
|
|
5841
|
+
Configure the role's appearance, assign members, and set its
|
|
5842
|
+
position in the hierarchy.
|
|
5843
|
+
</DialogDescription>
|
|
5844
|
+
</DialogHeader>
|
|
5845
|
+
|
|
5846
|
+
{/* ── Live preview ────────────────────────────────────────── */}
|
|
5847
|
+
<div className="px-6 py-3">
|
|
5848
|
+
<div className="flex items-center gap-3 rounded-lg border bg-muted/40 px-4 py-3">
|
|
5849
|
+
<span
|
|
5850
|
+
className="h-3.5 w-3.5 rounded-full shrink-0 transition-colors duration-200"
|
|
5851
|
+
style={{ backgroundColor: colorToHex(color) }}
|
|
5852
|
+
/>
|
|
5853
|
+
<span className="text-sm font-medium truncate">
|
|
5854
|
+
{name || "new role"}
|
|
5855
|
+
</span>
|
|
5856
|
+
<span
|
|
5857
|
+
className="ml-auto text-[11px] font-medium px-2.5 py-0.5 rounded-full transition-colors duration-200"
|
|
5858
|
+
style={{
|
|
5859
|
+
backgroundColor: colorToRgba(color, 0.12),
|
|
5860
|
+
color: colorToHex(color),
|
|
5861
|
+
border: \`1px solid \${colorToRgba(color, 0.25)}\`,
|
|
5862
|
+
}}
|
|
5863
|
+
>
|
|
5864
|
+
Preview
|
|
5865
|
+
</span>
|
|
5866
|
+
</div>
|
|
5867
|
+
</div>
|
|
5868
|
+
|
|
5869
|
+
{/* ── Scrollable body ─────────────────────────────────────── */}
|
|
5870
|
+
<div className="overflow-y-auto max-h-[60vh] px-6 pb-2 space-y-5">
|
|
5871
|
+
{/* ROLE NAME --------------------------------------------------- */}
|
|
5872
|
+
<fieldset>
|
|
5873
|
+
<legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
5874
|
+
Role Name
|
|
5875
|
+
</legend>
|
|
5876
|
+
<input
|
|
5877
|
+
type="text"
|
|
5878
|
+
value={name}
|
|
5879
|
+
onChange={(e) => setName(e.target.value)}
|
|
5880
|
+
placeholder="new role"
|
|
5881
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
5882
|
+
/>
|
|
5883
|
+
</fieldset>
|
|
5884
|
+
|
|
5885
|
+
{/* ROLE COLOR -------------------------------------------------- */}
|
|
5886
|
+
<fieldset>
|
|
5887
|
+
<legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
5888
|
+
Role Color
|
|
5889
|
+
</legend>
|
|
5890
|
+
|
|
5891
|
+
{/* Default (no color) toggle */}
|
|
5892
|
+
<button
|
|
5893
|
+
onClick={() => selectPreset(0)}
|
|
5894
|
+
className={cn(
|
|
5895
|
+
"mb-3 inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition-colors",
|
|
5896
|
+
color === 0
|
|
5897
|
+
? "border-primary bg-primary/10 text-primary"
|
|
5898
|
+
: "border-input text-muted-foreground hover:bg-accent",
|
|
5899
|
+
)}
|
|
5900
|
+
>
|
|
5901
|
+
<span
|
|
5902
|
+
className="h-4 w-4 rounded-full border-2 transition-colors"
|
|
5903
|
+
style={{
|
|
5904
|
+
borderColor: "#99AAB5",
|
|
5905
|
+
backgroundColor: color === 0 ? "#99AAB5" : "transparent",
|
|
5906
|
+
}}
|
|
5907
|
+
/>
|
|
5908
|
+
Default
|
|
5909
|
+
</button>
|
|
5910
|
+
|
|
5911
|
+
{/* Preset grid (2 rows of 10) */}
|
|
5912
|
+
<div className="grid grid-cols-10 gap-2 mb-3">
|
|
5913
|
+
{PRESET_COLORS.map((c) => (
|
|
5914
|
+
<button
|
|
5915
|
+
key={c}
|
|
5916
|
+
onClick={() => selectPreset(c)}
|
|
5917
|
+
className={cn(
|
|
5918
|
+
"h-7 w-7 rounded-full transition-all duration-150 hover:scale-110 focus-visible:outline-none relative",
|
|
5919
|
+
color === c &&
|
|
5920
|
+
"ring-2 ring-offset-2 ring-offset-background ring-primary scale-110",
|
|
5921
|
+
)}
|
|
5922
|
+
style={{ backgroundColor: colorToHex(c) }}
|
|
5923
|
+
title={colorToHex(c)}
|
|
5924
|
+
>
|
|
5925
|
+
{color === c && (
|
|
5926
|
+
<Check className="h-3 w-3 text-white absolute inset-0 m-auto drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" />
|
|
5927
|
+
)}
|
|
5928
|
+
</button>
|
|
5929
|
+
))}
|
|
5930
|
+
</div>
|
|
5931
|
+
|
|
5932
|
+
{/* Custom color */}
|
|
5933
|
+
<div className="flex items-center gap-2">
|
|
5934
|
+
<div className="relative">
|
|
5935
|
+
<input
|
|
5936
|
+
type="color"
|
|
5937
|
+
value={color !== 0 ? colorToHex(color) : "#000000"}
|
|
5938
|
+
onChange={(e) => {
|
|
5939
|
+
const hex = e.target.value;
|
|
5940
|
+
setColor(hexToColor(hex));
|
|
5941
|
+
setCustomHex(hex);
|
|
5942
|
+
setUsingCustom(true);
|
|
5943
|
+
}}
|
|
5944
|
+
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
5945
|
+
/>
|
|
5946
|
+
<div
|
|
5947
|
+
className={cn(
|
|
5948
|
+
"h-7 w-7 rounded-full border-2 flex items-center justify-center cursor-pointer transition-colors",
|
|
5949
|
+
usingCustom
|
|
5950
|
+
? "border-primary"
|
|
5951
|
+
: "border-dashed border-muted-foreground/40 hover:border-muted-foreground",
|
|
5952
|
+
)}
|
|
5953
|
+
style={
|
|
5954
|
+
usingCustom && color !== 0
|
|
5955
|
+
? { backgroundColor: colorToHex(color), borderStyle: "solid" }
|
|
5956
|
+
: {}
|
|
5957
|
+
}
|
|
5958
|
+
>
|
|
5959
|
+
{!usingCustom && (
|
|
5960
|
+
<Plus className="h-3 w-3 text-muted-foreground" />
|
|
5961
|
+
)}
|
|
5962
|
+
</div>
|
|
5963
|
+
</div>
|
|
5964
|
+
<input
|
|
5965
|
+
type="text"
|
|
5966
|
+
value={customHex}
|
|
5967
|
+
onChange={(e) => {
|
|
5968
|
+
const v = e.target.value;
|
|
5969
|
+
setCustomHex(v);
|
|
5970
|
+
if (v.match(/^#?[0-9a-fA-F]{6}$/)) {
|
|
5971
|
+
setColor(hexToColor(v));
|
|
5972
|
+
setUsingCustom(true);
|
|
5973
|
+
}
|
|
5974
|
+
}}
|
|
5975
|
+
placeholder="#000000"
|
|
5976
|
+
maxLength={7}
|
|
5977
|
+
className="w-[88px] rounded-md border border-input bg-background px-2 py-1 text-sm font-mono shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
5978
|
+
/>
|
|
5979
|
+
</div>
|
|
5980
|
+
</fieldset>
|
|
5981
|
+
|
|
5982
|
+
{/* ADD MEMBERS ------------------------------------------------- */}
|
|
5983
|
+
<fieldset>
|
|
5984
|
+
<legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
5985
|
+
Add Members
|
|
5986
|
+
{selectedMembers.length > 0 && (
|
|
5987
|
+
<span className="ml-1.5 text-[11px] font-normal">
|
|
5988
|
+
({selectedMembers.length})
|
|
5989
|
+
</span>
|
|
5990
|
+
)}
|
|
5991
|
+
</legend>
|
|
5992
|
+
|
|
5993
|
+
{/* Selected member chips */}
|
|
5994
|
+
{selectedMembers.length > 0 && (
|
|
5995
|
+
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
5996
|
+
{selectedMembers.map((userId) => {
|
|
5997
|
+
const member = members?.find(
|
|
5998
|
+
(m) => m.discordUserId === userId,
|
|
5999
|
+
);
|
|
6000
|
+
if (!member) return null;
|
|
6001
|
+
const url = avatarUrl(member.discordUserId, member.avatar);
|
|
6002
|
+
return (
|
|
6003
|
+
<span
|
|
6004
|
+
key={userId}
|
|
6005
|
+
className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 text-primary pl-1 pr-1.5 py-0.5 text-xs font-medium"
|
|
6006
|
+
>
|
|
6007
|
+
{url ? (
|
|
6008
|
+
<img
|
|
6009
|
+
src={url}
|
|
6010
|
+
alt=""
|
|
6011
|
+
className="h-4 w-4 rounded-full shrink-0 object-cover"
|
|
6012
|
+
/>
|
|
6013
|
+
) : (
|
|
6014
|
+
<div className="h-4 w-4 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
|
6015
|
+
<User className="h-2.5 w-2.5" />
|
|
6016
|
+
</div>
|
|
6017
|
+
)}
|
|
6018
|
+
{member.displayName ?? member.username}
|
|
6019
|
+
<button
|
|
6020
|
+
onClick={() => toggleMember(userId)}
|
|
6021
|
+
className="rounded-full p-0.5 hover:bg-primary/20 transition-colors"
|
|
6022
|
+
>
|
|
6023
|
+
<X className="h-3 w-3" />
|
|
6024
|
+
</button>
|
|
6025
|
+
</span>
|
|
6026
|
+
);
|
|
6027
|
+
})}
|
|
6028
|
+
</div>
|
|
6029
|
+
)}
|
|
6030
|
+
|
|
6031
|
+
{/* Member combobox */}
|
|
6032
|
+
<Popover open={memberPickerOpen} onOpenChange={setMemberPickerOpen}>
|
|
6033
|
+
<PopoverTrigger asChild>
|
|
6034
|
+
<Button
|
|
6035
|
+
variant="outline"
|
|
6036
|
+
role="combobox"
|
|
6037
|
+
aria-expanded={memberPickerOpen}
|
|
6038
|
+
className="w-full justify-between font-normal"
|
|
6039
|
+
>
|
|
6040
|
+
<span className="text-muted-foreground">
|
|
6041
|
+
Search by name or ID...
|
|
6042
|
+
</span>
|
|
6043
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
6044
|
+
</Button>
|
|
6045
|
+
</PopoverTrigger>
|
|
6046
|
+
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
|
6047
|
+
<Command>
|
|
6048
|
+
<CommandInput placeholder="Search members..." />
|
|
6049
|
+
<CommandList>
|
|
6050
|
+
<CommandEmpty>No members found.</CommandEmpty>
|
|
6051
|
+
<CommandGroup>
|
|
6052
|
+
{members?.map((member) => {
|
|
6053
|
+
const checked = selectedMembers.includes(
|
|
6054
|
+
member.discordUserId,
|
|
6055
|
+
);
|
|
6056
|
+
const url = avatarUrl(
|
|
6057
|
+
member.discordUserId,
|
|
6058
|
+
member.avatar,
|
|
6059
|
+
);
|
|
6060
|
+
return (
|
|
6061
|
+
<CommandItem
|
|
6062
|
+
key={member.discordUserId}
|
|
6063
|
+
value={\`\${member.discordUserId} \${member.username} \${member.displayName ?? ""} \${member.nick ?? ""}\`}
|
|
6064
|
+
onSelect={() => toggleMember(member.discordUserId)}
|
|
6065
|
+
>
|
|
6066
|
+
{url ? (
|
|
6067
|
+
<img
|
|
6068
|
+
src={url}
|
|
6069
|
+
alt=""
|
|
6070
|
+
className="mr-2 h-5 w-5 rounded-full shrink-0 object-cover"
|
|
6071
|
+
/>
|
|
6072
|
+
) : (
|
|
6073
|
+
<div className="mr-2 h-5 w-5 rounded-full bg-muted flex items-center justify-center shrink-0">
|
|
6074
|
+
<User className="h-3 w-3 text-muted-foreground" />
|
|
6075
|
+
</div>
|
|
6076
|
+
)}
|
|
6077
|
+
<span className="truncate">
|
|
6078
|
+
{member.displayName ?? member.username}
|
|
6079
|
+
</span>
|
|
6080
|
+
<Check
|
|
6081
|
+
className={cn(
|
|
6082
|
+
"ml-auto h-4 w-4",
|
|
6083
|
+
checked ? "opacity-100" : "opacity-0",
|
|
6084
|
+
)}
|
|
6085
|
+
/>
|
|
6086
|
+
</CommandItem>
|
|
6087
|
+
);
|
|
6088
|
+
})}
|
|
6089
|
+
</CommandGroup>
|
|
6090
|
+
</CommandList>
|
|
6091
|
+
</Command>
|
|
6092
|
+
</PopoverContent>
|
|
6093
|
+
</Popover>
|
|
6094
|
+
</fieldset>
|
|
6095
|
+
|
|
6096
|
+
{/* ROLE POSITION ------------------------------------------------ */}
|
|
6097
|
+
<fieldset>
|
|
6098
|
+
<div className="flex items-center justify-between mb-2">
|
|
6099
|
+
<legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
6100
|
+
Role Position
|
|
6101
|
+
</legend>
|
|
6102
|
+
<div className="flex items-center gap-0.5">
|
|
6103
|
+
<button
|
|
6104
|
+
onClick={moveUp}
|
|
6105
|
+
disabled={positionIndex === 0}
|
|
6106
|
+
className="rounded-md p-1 hover:bg-accent disabled:opacity-25 disabled:pointer-events-none transition-colors"
|
|
6107
|
+
title="Move up"
|
|
6108
|
+
>
|
|
6109
|
+
<ChevronUp className="h-4 w-4" />
|
|
6110
|
+
</button>
|
|
6111
|
+
<button
|
|
6112
|
+
onClick={moveDown}
|
|
6113
|
+
disabled={positionIndex >= sortedRoles.length}
|
|
6114
|
+
className="rounded-md p-1 hover:bg-accent disabled:opacity-25 disabled:pointer-events-none transition-colors"
|
|
6115
|
+
title="Move down"
|
|
6116
|
+
>
|
|
6117
|
+
<ChevronDown className="h-4 w-4" />
|
|
6118
|
+
</button>
|
|
6119
|
+
</div>
|
|
6120
|
+
</div>
|
|
6121
|
+
|
|
6122
|
+
<div className="max-h-[200px] overflow-y-auto rounded-md border border-input">
|
|
6123
|
+
{sortedRoles.length === 0 && roles !== undefined ? (
|
|
6124
|
+
<>
|
|
6125
|
+
<NewRoleEntry
|
|
6126
|
+
name={name}
|
|
6127
|
+
color={color}
|
|
6128
|
+
/>
|
|
6129
|
+
<EveryoneEntry />
|
|
6130
|
+
</>
|
|
6131
|
+
) : sortedRoles.length === 0 ? (
|
|
6132
|
+
<div className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
6133
|
+
Loading roles...
|
|
6134
|
+
</div>
|
|
6135
|
+
) : (
|
|
6136
|
+
<>
|
|
6137
|
+
{sortedRoles.map((role, i) => (
|
|
6138
|
+
<div key={role.discordId}>
|
|
6139
|
+
{positionIndex === i && (
|
|
6140
|
+
<NewRoleEntry
|
|
6141
|
+
name={name}
|
|
6142
|
+
color={color}
|
|
6143
|
+
/>
|
|
6144
|
+
)}
|
|
6145
|
+
<div className="flex items-center gap-2.5 px-3 py-2 text-sm">
|
|
6146
|
+
<span
|
|
6147
|
+
className="h-3 w-3 rounded-full shrink-0"
|
|
6148
|
+
style={{ backgroundColor: colorToHex(role.color) }}
|
|
6149
|
+
/>
|
|
6150
|
+
<span className="truncate flex-1">{role.name}</span>
|
|
6151
|
+
<span className="text-[11px] tabular-nums text-muted-foreground">
|
|
6152
|
+
{role.position}
|
|
6153
|
+
</span>
|
|
6154
|
+
</div>
|
|
6155
|
+
</div>
|
|
6156
|
+
))}
|
|
6157
|
+
{positionIndex >= sortedRoles.length && (
|
|
6158
|
+
<NewRoleEntry
|
|
6159
|
+
name={name}
|
|
6160
|
+
color={color}
|
|
6161
|
+
/>
|
|
6162
|
+
)}
|
|
6163
|
+
<EveryoneEntry />
|
|
6164
|
+
</>
|
|
6165
|
+
)}
|
|
6166
|
+
</div>
|
|
6167
|
+
</fieldset>
|
|
6168
|
+
</div>
|
|
6169
|
+
|
|
6170
|
+
{/* ── Footer ──────────────────────────────────────────────── */}
|
|
6171
|
+
<div className="flex items-center justify-end gap-3 border-t px-6 py-4 mt-1">
|
|
6172
|
+
<Button variant="ghost" onClick={() => setOpen(false)}>
|
|
6173
|
+
Cancel
|
|
6174
|
+
</Button>
|
|
6175
|
+
<Button
|
|
6176
|
+
onClick={handleCreate}
|
|
6177
|
+
disabled={!name.trim()}
|
|
6178
|
+
className="transition-colors duration-200"
|
|
6179
|
+
style={
|
|
6180
|
+
color !== 0
|
|
6181
|
+
? { backgroundColor: colorToHex(color), color: "#fff" }
|
|
6182
|
+
: undefined
|
|
6183
|
+
}
|
|
6184
|
+
>
|
|
6185
|
+
Create Role
|
|
6186
|
+
</Button>
|
|
6187
|
+
</div>
|
|
6188
|
+
</DialogContent>
|
|
6189
|
+
</Dialog>
|
|
6190
|
+
);
|
|
6191
|
+
}
|
|
6192
|
+
|
|
6193
|
+
// ── Sub-components ─────────────────────────────────────────────────────────
|
|
6194
|
+
|
|
6195
|
+
function NewRoleEntry({ name, color }: { name: string; color: number }) {
|
|
6196
|
+
return (
|
|
6197
|
+
<div className="flex items-center gap-2.5 px-3 py-2 bg-primary/[0.08] border-l-2 border-l-primary">
|
|
6198
|
+
<span
|
|
6199
|
+
className="h-3 w-3 rounded-full shrink-0 transition-colors duration-200"
|
|
6200
|
+
style={{ backgroundColor: colorToHex(color) }}
|
|
6201
|
+
/>
|
|
6202
|
+
<span className="text-sm font-medium truncate flex-1">
|
|
6203
|
+
{name || "new role"}
|
|
6204
|
+
</span>
|
|
6205
|
+
<span className="text-[11px] font-semibold text-primary">NEW</span>
|
|
6206
|
+
</div>
|
|
6207
|
+
);
|
|
6208
|
+
}
|
|
6209
|
+
|
|
6210
|
+
function EveryoneEntry() {
|
|
6211
|
+
return (
|
|
6212
|
+
<div className="flex items-center gap-2.5 px-3 py-2 opacity-40">
|
|
6213
|
+
<span className="h-3 w-3 rounded-full shrink-0 bg-muted-foreground/50" />
|
|
6214
|
+
<span className="text-sm truncate">@everyone</span>
|
|
6215
|
+
<span className="ml-auto text-[11px] tabular-nums">0</span>
|
|
6216
|
+
</div>
|
|
6217
|
+
);
|
|
6218
|
+
}
|
|
6219
|
+
`,
|
|
6220
|
+
generateConvexQueries: () => `export const listRolesForCreator = query({
|
|
6221
|
+
args: { guildDiscordId: v.string() },
|
|
6222
|
+
returns: v.array(
|
|
6223
|
+
v.object({
|
|
6224
|
+
discordId: v.string(),
|
|
6225
|
+
name: v.string(),
|
|
6226
|
+
color: v.number(),
|
|
6227
|
+
position: v.number(),
|
|
6228
|
+
})
|
|
6229
|
+
),
|
|
6230
|
+
handler: async (ctx, { guildDiscordId }) => {
|
|
6231
|
+
const roles = await ctx.db
|
|
6232
|
+
.query("calabasasRoles")
|
|
6233
|
+
.withIndex("by_guild", (q) => q.eq("guildDiscordId", guildDiscordId))
|
|
6234
|
+
.collect();
|
|
6235
|
+
return roles.map((r) => ({
|
|
6236
|
+
discordId: r.discordId,
|
|
6237
|
+
name: r.name,
|
|
6238
|
+
color: r.color,
|
|
6239
|
+
position: r.position,
|
|
6240
|
+
}));
|
|
6241
|
+
},
|
|
6242
|
+
});
|
|
6243
|
+
|
|
6244
|
+
export const listMembersForCreator = query({
|
|
6245
|
+
args: { guildDiscordId: v.string() },
|
|
6246
|
+
returns: v.array(
|
|
6247
|
+
v.object({
|
|
6248
|
+
discordUserId: v.string(),
|
|
6249
|
+
username: v.string(),
|
|
6250
|
+
displayName: v.optional(v.string()),
|
|
6251
|
+
avatar: v.optional(v.string()),
|
|
6252
|
+
nick: v.optional(v.string()),
|
|
6253
|
+
})
|
|
6254
|
+
),
|
|
6255
|
+
handler: async (ctx, { guildDiscordId }) => {
|
|
6256
|
+
const members = await ctx.db
|
|
6257
|
+
.query("calabasasMembers")
|
|
6258
|
+
.withIndex("by_guild", (q) => q.eq("guildDiscordId", guildDiscordId))
|
|
6259
|
+
.collect();
|
|
6260
|
+
return members.map((m) => ({
|
|
6261
|
+
discordUserId: m.discordUserId,
|
|
6262
|
+
username: m.username,
|
|
6263
|
+
displayName: m.displayName,
|
|
6264
|
+
avatar: m.avatar,
|
|
6265
|
+
nick: m.nick,
|
|
6266
|
+
}));
|
|
6267
|
+
},
|
|
6268
|
+
});`
|
|
6269
|
+
};
|
|
6270
|
+
|
|
5744
6271
|
// src/lib/registry/index.ts
|
|
5745
6272
|
var REGISTRY = [
|
|
5746
6273
|
channelSelect,
|
|
@@ -5754,7 +6281,8 @@ var REGISTRY = [
|
|
|
5754
6281
|
channelTree,
|
|
5755
6282
|
memberRoster,
|
|
5756
6283
|
useOnlineCount,
|
|
5757
|
-
emojiPicker
|
|
6284
|
+
emojiPicker,
|
|
6285
|
+
roleCreator
|
|
5758
6286
|
];
|
|
5759
6287
|
function getComponent(name) {
|
|
5760
6288
|
return REGISTRY.find((c) => c.name === name);
|
|
@@ -7187,7 +7715,7 @@ program2.command("logout").description("Clear stored credentials").option("--dev
|
|
|
7187
7715
|
program2.command("init").description("Initialize Calabasas config in your Convex project").action(init);
|
|
7188
7716
|
program2.command("push").description("Push your Calabasas config to the server").option("-c, --config <path>", "Path to config file", "convex/calabasas/config.ts").option("-b, --bot <botId>", "Bot ID to configure (prompts if not specified)").option("--dev", "Push to development environment").option("--prod", "Push to production environment").action(push);
|
|
7189
7717
|
program2.command("generate").description("Generate type-safe Discord handlers and helpers").option("-o, --output <path>", "Output path", "convex/calabasas/_generated/discord.ts").option("--dev", "Use development environment").option("--prod", "Use production environment").action(generate);
|
|
7190
|
-
program2.command("skill").description("Generate Calabasas
|
|
7718
|
+
program2.command("skill").description("Generate Calabasas skill for AI assistants").option("--dev", "Use development environment").option("--prod", "Use production environment").action(skill);
|
|
7191
7719
|
program2.command("add [components...]").description("Add Discord UI components to your project").action(add);
|
|
7192
7720
|
program2.command("migrate [name]").description("Run a codemod migration (list available if no name given)").action(migrate);
|
|
7193
7721
|
program2.command("config").description("View and modify your Calabasas config").option("-c, --config <path>", "Path to config file", "convex/calabasas/config.ts").option("-l, --list", "List current and available config options").option("--add-event <events...>", "Enable event handlers").option("--remove-event <events...>", "Disable event handlers").option("--add-sync <properties...>", "Enable sync properties").option("--remove-sync <properties...>", "Disable sync properties").action(config);
|