calabasas 0.16.1 → 0.17.0
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 +648 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5741,6 +5741,652 @@ export const listAppEmojis = query({
|
|
|
5741
5741
|
});`
|
|
5742
5742
|
};
|
|
5743
5743
|
|
|
5744
|
+
// src/lib/registry/components/role-creator.ts
|
|
5745
|
+
var roleCreator = {
|
|
5746
|
+
name: "role-creator",
|
|
5747
|
+
kind: "component",
|
|
5748
|
+
description: "Discord-style role creation dialog with color picker, member assignment, and position ordering",
|
|
5749
|
+
requiredSyncTypes: ["roles", "members"],
|
|
5750
|
+
requiredShadcnComponents: ["dialog", "button", "popover", "command"],
|
|
5751
|
+
generateReactComponent: () => `"use client";
|
|
5752
|
+
|
|
5753
|
+
import { useState, useMemo } from "react";
|
|
5754
|
+
import { useQuery } from "convex/react";
|
|
5755
|
+
import { api } from "@/convex/_generated/api";
|
|
5756
|
+
import {
|
|
5757
|
+
Dialog,
|
|
5758
|
+
DialogContent,
|
|
5759
|
+
DialogDescription,
|
|
5760
|
+
DialogHeader,
|
|
5761
|
+
DialogTitle,
|
|
5762
|
+
DialogTrigger,
|
|
5763
|
+
} from "@/components/ui/dialog";
|
|
5764
|
+
import {
|
|
5765
|
+
Popover,
|
|
5766
|
+
PopoverContent,
|
|
5767
|
+
PopoverTrigger,
|
|
5768
|
+
} from "@/components/ui/popover";
|
|
5769
|
+
import {
|
|
5770
|
+
Command,
|
|
5771
|
+
CommandEmpty,
|
|
5772
|
+
CommandGroup,
|
|
5773
|
+
CommandInput,
|
|
5774
|
+
CommandItem,
|
|
5775
|
+
CommandList,
|
|
5776
|
+
} from "@/components/ui/command";
|
|
5777
|
+
import { Button } from "@/components/ui/button";
|
|
5778
|
+
import { cn } from "@/lib/utils";
|
|
5779
|
+
import {
|
|
5780
|
+
Check,
|
|
5781
|
+
X,
|
|
5782
|
+
ChevronsUpDown,
|
|
5783
|
+
User,
|
|
5784
|
+
Plus,
|
|
5785
|
+
ChevronUp,
|
|
5786
|
+
ChevronDown,
|
|
5787
|
+
} from "lucide-react";
|
|
5788
|
+
|
|
5789
|
+
/**
|
|
5790
|
+
* Discord's 20 preset role colors arranged in two rows:
|
|
5791
|
+
* Row 1 — lighter tones, Row 2 — darker counterparts.
|
|
5792
|
+
*/
|
|
5793
|
+
const PRESET_COLORS: number[] = [
|
|
5794
|
+
0x1abc9c, 0x2ecc71, 0x3498db, 0x9b59b6, 0xe91e63,
|
|
5795
|
+
0xf1c40f, 0xe67e22, 0xe74c3c, 0x95a5a6, 0x607d8b,
|
|
5796
|
+
0x11806a, 0x1f8b4c, 0x206694, 0x71368a, 0xad1457,
|
|
5797
|
+
0xc27c0e, 0xa84300, 0x992d22, 0x979c9f, 0x546e7a,
|
|
5798
|
+
];
|
|
5799
|
+
|
|
5800
|
+
/** Convert Discord integer color to hex CSS string */
|
|
5801
|
+
function colorToHex(color: number): string {
|
|
5802
|
+
if (color === 0) return "#99AAB5";
|
|
5803
|
+
return "#" + color.toString(16).padStart(6, "0");
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
/** Convert hex string to Discord integer color */
|
|
5807
|
+
function hexToColor(hex: string): number {
|
|
5808
|
+
const clean = hex.replace("#", "");
|
|
5809
|
+
const parsed = parseInt(clean, 16);
|
|
5810
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
/** Convert Discord integer color to rgba string */
|
|
5814
|
+
function colorToRgba(color: number, alpha: number): string {
|
|
5815
|
+
if (color === 0) return \`rgba(153, 170, 181, \${alpha})\`;
|
|
5816
|
+
const r = (color >> 16) & 0xff;
|
|
5817
|
+
const g = (color >> 8) & 0xff;
|
|
5818
|
+
const b = color & 0xff;
|
|
5819
|
+
return \`rgba(\${r}, \${g}, \${b}, \${alpha})\`;
|
|
5820
|
+
}
|
|
5821
|
+
|
|
5822
|
+
/** Build Discord CDN avatar URL */
|
|
5823
|
+
function avatarUrl(
|
|
5824
|
+
userId: string,
|
|
5825
|
+
avatar: string | undefined,
|
|
5826
|
+
): string | null {
|
|
5827
|
+
if (!avatar) return null;
|
|
5828
|
+
return \`https://cdn.discordapp.com/avatars/\${userId}/\${avatar}.png?size=32\`;
|
|
5829
|
+
}
|
|
5830
|
+
|
|
5831
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
5832
|
+
|
|
5833
|
+
type RoleCreatorData = {
|
|
5834
|
+
name: string;
|
|
5835
|
+
color: number;
|
|
5836
|
+
memberIds: string[];
|
|
5837
|
+
position: number;
|
|
5838
|
+
};
|
|
5839
|
+
|
|
5840
|
+
type RoleCreatorProps = {
|
|
5841
|
+
guildDiscordId: string;
|
|
5842
|
+
defaultMemberIds?: string[];
|
|
5843
|
+
open?: boolean;
|
|
5844
|
+
onOpenChange?: (open: boolean) => void;
|
|
5845
|
+
onCreate?: (data: RoleCreatorData) => void;
|
|
5846
|
+
trigger?: React.ReactNode;
|
|
5847
|
+
className?: string;
|
|
5848
|
+
};
|
|
5849
|
+
|
|
5850
|
+
// ── Component ────────────────────────────────────────────────────────────────
|
|
5851
|
+
|
|
5852
|
+
export function RoleCreator({
|
|
5853
|
+
guildDiscordId,
|
|
5854
|
+
defaultMemberIds = [],
|
|
5855
|
+
open: controlledOpen,
|
|
5856
|
+
onOpenChange,
|
|
5857
|
+
onCreate,
|
|
5858
|
+
trigger,
|
|
5859
|
+
className,
|
|
5860
|
+
}: RoleCreatorProps) {
|
|
5861
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
5862
|
+
const isControlled = controlledOpen !== undefined;
|
|
5863
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
5864
|
+
|
|
5865
|
+
// Form state
|
|
5866
|
+
const [name, setName] = useState("new role");
|
|
5867
|
+
const [color, setColor] = useState(0);
|
|
5868
|
+
const [customHex, setCustomHex] = useState("");
|
|
5869
|
+
const [usingCustom, setUsingCustom] = useState(false);
|
|
5870
|
+
const [selectedMembers, setSelectedMembers] = useState<string[]>(defaultMemberIds);
|
|
5871
|
+
const [memberPickerOpen, setMemberPickerOpen] = useState(false);
|
|
5872
|
+
const [positionIndex, setPositionIndex] = useState(0);
|
|
5873
|
+
|
|
5874
|
+
// Data
|
|
5875
|
+
const roles = useQuery(api.calabasas.queries.listRolesForCreator, {
|
|
5876
|
+
guildDiscordId,
|
|
5877
|
+
});
|
|
5878
|
+
const members = useQuery(api.calabasas.queries.listMembersForCreator, {
|
|
5879
|
+
guildDiscordId,
|
|
5880
|
+
});
|
|
5881
|
+
|
|
5882
|
+
// Sort roles highest-position first, exclude @everyone
|
|
5883
|
+
const sortedRoles = useMemo(() => {
|
|
5884
|
+
if (!roles) return [];
|
|
5885
|
+
return roles
|
|
5886
|
+
.filter((r) => r.name !== "@everyone")
|
|
5887
|
+
.sort((a, b) => b.position - a.position);
|
|
5888
|
+
}, [roles]);
|
|
5889
|
+
|
|
5890
|
+
// Calculate the position value from the insertion index
|
|
5891
|
+
const calculatedPosition = useMemo(() => {
|
|
5892
|
+
if (sortedRoles.length === 0) return 1;
|
|
5893
|
+
if (positionIndex === 0) return sortedRoles[0].position + 1;
|
|
5894
|
+
if (positionIndex >= sortedRoles.length) return 1;
|
|
5895
|
+
return sortedRoles[positionIndex - 1].position;
|
|
5896
|
+
}, [sortedRoles, positionIndex]);
|
|
5897
|
+
|
|
5898
|
+
const reset = () => {
|
|
5899
|
+
setName("new role");
|
|
5900
|
+
setColor(0);
|
|
5901
|
+
setCustomHex("");
|
|
5902
|
+
setUsingCustom(false);
|
|
5903
|
+
setSelectedMembers(defaultMemberIds);
|
|
5904
|
+
setMemberPickerOpen(false);
|
|
5905
|
+
setPositionIndex(0);
|
|
5906
|
+
};
|
|
5907
|
+
|
|
5908
|
+
const setOpen = (v: boolean) => {
|
|
5909
|
+
if (!v) reset();
|
|
5910
|
+
if (!isControlled) setInternalOpen(v);
|
|
5911
|
+
onOpenChange?.(v);
|
|
5912
|
+
};
|
|
5913
|
+
|
|
5914
|
+
const handleCreate = () => {
|
|
5915
|
+
onCreate?.({
|
|
5916
|
+
name,
|
|
5917
|
+
color,
|
|
5918
|
+
memberIds: selectedMembers,
|
|
5919
|
+
position: calculatedPosition,
|
|
5920
|
+
});
|
|
5921
|
+
setOpen(false);
|
|
5922
|
+
};
|
|
5923
|
+
|
|
5924
|
+
const toggleMember = (userId: string) => {
|
|
5925
|
+
setSelectedMembers((prev) =>
|
|
5926
|
+
prev.includes(userId)
|
|
5927
|
+
? prev.filter((id) => id !== userId)
|
|
5928
|
+
: [...prev, userId],
|
|
5929
|
+
);
|
|
5930
|
+
};
|
|
5931
|
+
|
|
5932
|
+
const selectPreset = (c: number) => {
|
|
5933
|
+
setColor(c);
|
|
5934
|
+
setUsingCustom(false);
|
|
5935
|
+
setCustomHex("");
|
|
5936
|
+
};
|
|
5937
|
+
|
|
5938
|
+
const moveUp = () => setPositionIndex((i) => Math.max(0, i - 1));
|
|
5939
|
+
const moveDown = () =>
|
|
5940
|
+
setPositionIndex((i) => Math.min(sortedRoles.length, i + 1));
|
|
5941
|
+
|
|
5942
|
+
// ── Render ───────────────────────────────────────────────────────────────
|
|
5943
|
+
|
|
5944
|
+
return (
|
|
5945
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
5946
|
+
<DialogTrigger asChild>
|
|
5947
|
+
{trigger ?? (
|
|
5948
|
+
<Button variant="outline" className={className}>
|
|
5949
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
5950
|
+
Create Role
|
|
5951
|
+
</Button>
|
|
5952
|
+
)}
|
|
5953
|
+
</DialogTrigger>
|
|
5954
|
+
|
|
5955
|
+
<DialogContent className="sm:max-w-[520px] p-0 gap-0 overflow-hidden">
|
|
5956
|
+
{/* ── Header ──────────────────────────────────────────────── */}
|
|
5957
|
+
<DialogHeader className="px-6 pt-6 pb-2">
|
|
5958
|
+
<DialogTitle>Create Role</DialogTitle>
|
|
5959
|
+
<DialogDescription>
|
|
5960
|
+
Configure the role's appearance, assign members, and set its
|
|
5961
|
+
position in the hierarchy.
|
|
5962
|
+
</DialogDescription>
|
|
5963
|
+
</DialogHeader>
|
|
5964
|
+
|
|
5965
|
+
{/* ── Live preview ────────────────────────────────────────── */}
|
|
5966
|
+
<div className="px-6 py-3">
|
|
5967
|
+
<div className="flex items-center gap-3 rounded-lg border bg-muted/40 px-4 py-3">
|
|
5968
|
+
<span
|
|
5969
|
+
className="h-3.5 w-3.5 rounded-full shrink-0 transition-colors duration-200"
|
|
5970
|
+
style={{ backgroundColor: colorToHex(color) }}
|
|
5971
|
+
/>
|
|
5972
|
+
<span className="text-sm font-medium truncate">
|
|
5973
|
+
{name || "new role"}
|
|
5974
|
+
</span>
|
|
5975
|
+
<span
|
|
5976
|
+
className="ml-auto text-[11px] font-medium px-2.5 py-0.5 rounded-full transition-colors duration-200"
|
|
5977
|
+
style={{
|
|
5978
|
+
backgroundColor: colorToRgba(color, 0.12),
|
|
5979
|
+
color: colorToHex(color),
|
|
5980
|
+
border: \`1px solid \${colorToRgba(color, 0.25)}\`,
|
|
5981
|
+
}}
|
|
5982
|
+
>
|
|
5983
|
+
Preview
|
|
5984
|
+
</span>
|
|
5985
|
+
</div>
|
|
5986
|
+
</div>
|
|
5987
|
+
|
|
5988
|
+
{/* ── Scrollable body ─────────────────────────────────────── */}
|
|
5989
|
+
<div className="overflow-y-auto max-h-[60vh] px-6 pb-2 space-y-5">
|
|
5990
|
+
{/* ROLE NAME --------------------------------------------------- */}
|
|
5991
|
+
<fieldset>
|
|
5992
|
+
<legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
5993
|
+
Role Name
|
|
5994
|
+
</legend>
|
|
5995
|
+
<input
|
|
5996
|
+
type="text"
|
|
5997
|
+
value={name}
|
|
5998
|
+
onChange={(e) => setName(e.target.value)}
|
|
5999
|
+
placeholder="new role"
|
|
6000
|
+
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"
|
|
6001
|
+
/>
|
|
6002
|
+
</fieldset>
|
|
6003
|
+
|
|
6004
|
+
{/* ROLE COLOR -------------------------------------------------- */}
|
|
6005
|
+
<fieldset>
|
|
6006
|
+
<legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
6007
|
+
Role Color
|
|
6008
|
+
</legend>
|
|
6009
|
+
|
|
6010
|
+
{/* Default (no color) toggle */}
|
|
6011
|
+
<button
|
|
6012
|
+
onClick={() => selectPreset(0)}
|
|
6013
|
+
className={cn(
|
|
6014
|
+
"mb-3 inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition-colors",
|
|
6015
|
+
color === 0
|
|
6016
|
+
? "border-primary bg-primary/10 text-primary"
|
|
6017
|
+
: "border-input text-muted-foreground hover:bg-accent",
|
|
6018
|
+
)}
|
|
6019
|
+
>
|
|
6020
|
+
<span
|
|
6021
|
+
className="h-4 w-4 rounded-full border-2 transition-colors"
|
|
6022
|
+
style={{
|
|
6023
|
+
borderColor: "#99AAB5",
|
|
6024
|
+
backgroundColor: color === 0 ? "#99AAB5" : "transparent",
|
|
6025
|
+
}}
|
|
6026
|
+
/>
|
|
6027
|
+
Default
|
|
6028
|
+
</button>
|
|
6029
|
+
|
|
6030
|
+
{/* Preset grid (2 rows of 10) */}
|
|
6031
|
+
<div className="grid grid-cols-10 gap-2 mb-3">
|
|
6032
|
+
{PRESET_COLORS.map((c) => (
|
|
6033
|
+
<button
|
|
6034
|
+
key={c}
|
|
6035
|
+
onClick={() => selectPreset(c)}
|
|
6036
|
+
className={cn(
|
|
6037
|
+
"h-7 w-7 rounded-full transition-all duration-150 hover:scale-110 focus-visible:outline-none relative",
|
|
6038
|
+
color === c &&
|
|
6039
|
+
"ring-2 ring-offset-2 ring-offset-background ring-primary scale-110",
|
|
6040
|
+
)}
|
|
6041
|
+
style={{ backgroundColor: colorToHex(c) }}
|
|
6042
|
+
title={colorToHex(c)}
|
|
6043
|
+
>
|
|
6044
|
+
{color === c && (
|
|
6045
|
+
<Check className="h-3 w-3 text-white absolute inset-0 m-auto drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" />
|
|
6046
|
+
)}
|
|
6047
|
+
</button>
|
|
6048
|
+
))}
|
|
6049
|
+
</div>
|
|
6050
|
+
|
|
6051
|
+
{/* Custom color */}
|
|
6052
|
+
<div className="flex items-center gap-2">
|
|
6053
|
+
<div className="relative">
|
|
6054
|
+
<input
|
|
6055
|
+
type="color"
|
|
6056
|
+
value={color !== 0 ? colorToHex(color) : "#000000"}
|
|
6057
|
+
onChange={(e) => {
|
|
6058
|
+
const hex = e.target.value;
|
|
6059
|
+
setColor(hexToColor(hex));
|
|
6060
|
+
setCustomHex(hex);
|
|
6061
|
+
setUsingCustom(true);
|
|
6062
|
+
}}
|
|
6063
|
+
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
6064
|
+
/>
|
|
6065
|
+
<div
|
|
6066
|
+
className={cn(
|
|
6067
|
+
"h-7 w-7 rounded-full border-2 flex items-center justify-center cursor-pointer transition-colors",
|
|
6068
|
+
usingCustom
|
|
6069
|
+
? "border-primary"
|
|
6070
|
+
: "border-dashed border-muted-foreground/40 hover:border-muted-foreground",
|
|
6071
|
+
)}
|
|
6072
|
+
style={
|
|
6073
|
+
usingCustom && color !== 0
|
|
6074
|
+
? { backgroundColor: colorToHex(color), borderStyle: "solid" }
|
|
6075
|
+
: {}
|
|
6076
|
+
}
|
|
6077
|
+
>
|
|
6078
|
+
{!usingCustom && (
|
|
6079
|
+
<Plus className="h-3 w-3 text-muted-foreground" />
|
|
6080
|
+
)}
|
|
6081
|
+
</div>
|
|
6082
|
+
</div>
|
|
6083
|
+
<input
|
|
6084
|
+
type="text"
|
|
6085
|
+
value={customHex}
|
|
6086
|
+
onChange={(e) => {
|
|
6087
|
+
const v = e.target.value;
|
|
6088
|
+
setCustomHex(v);
|
|
6089
|
+
if (v.match(/^#?[0-9a-fA-F]{6}$/)) {
|
|
6090
|
+
setColor(hexToColor(v));
|
|
6091
|
+
setUsingCustom(true);
|
|
6092
|
+
}
|
|
6093
|
+
}}
|
|
6094
|
+
placeholder="#000000"
|
|
6095
|
+
maxLength={7}
|
|
6096
|
+
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"
|
|
6097
|
+
/>
|
|
6098
|
+
</div>
|
|
6099
|
+
</fieldset>
|
|
6100
|
+
|
|
6101
|
+
{/* ADD MEMBERS ------------------------------------------------- */}
|
|
6102
|
+
<fieldset>
|
|
6103
|
+
<legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
6104
|
+
Add Members
|
|
6105
|
+
{selectedMembers.length > 0 && (
|
|
6106
|
+
<span className="ml-1.5 text-[11px] font-normal">
|
|
6107
|
+
({selectedMembers.length})
|
|
6108
|
+
</span>
|
|
6109
|
+
)}
|
|
6110
|
+
</legend>
|
|
6111
|
+
|
|
6112
|
+
{/* Selected member chips */}
|
|
6113
|
+
{selectedMembers.length > 0 && (
|
|
6114
|
+
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
6115
|
+
{selectedMembers.map((userId) => {
|
|
6116
|
+
const member = members?.find(
|
|
6117
|
+
(m) => m.discordUserId === userId,
|
|
6118
|
+
);
|
|
6119
|
+
if (!member) return null;
|
|
6120
|
+
const url = avatarUrl(member.discordUserId, member.avatar);
|
|
6121
|
+
return (
|
|
6122
|
+
<span
|
|
6123
|
+
key={userId}
|
|
6124
|
+
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"
|
|
6125
|
+
>
|
|
6126
|
+
{url ? (
|
|
6127
|
+
<img
|
|
6128
|
+
src={url}
|
|
6129
|
+
alt=""
|
|
6130
|
+
className="h-4 w-4 rounded-full shrink-0 object-cover"
|
|
6131
|
+
/>
|
|
6132
|
+
) : (
|
|
6133
|
+
<div className="h-4 w-4 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
|
6134
|
+
<User className="h-2.5 w-2.5" />
|
|
6135
|
+
</div>
|
|
6136
|
+
)}
|
|
6137
|
+
{member.displayName ?? member.username}
|
|
6138
|
+
<button
|
|
6139
|
+
onClick={() => toggleMember(userId)}
|
|
6140
|
+
className="rounded-full p-0.5 hover:bg-primary/20 transition-colors"
|
|
6141
|
+
>
|
|
6142
|
+
<X className="h-3 w-3" />
|
|
6143
|
+
</button>
|
|
6144
|
+
</span>
|
|
6145
|
+
);
|
|
6146
|
+
})}
|
|
6147
|
+
</div>
|
|
6148
|
+
)}
|
|
6149
|
+
|
|
6150
|
+
{/* Member combobox */}
|
|
6151
|
+
<Popover open={memberPickerOpen} onOpenChange={setMemberPickerOpen}>
|
|
6152
|
+
<PopoverTrigger asChild>
|
|
6153
|
+
<Button
|
|
6154
|
+
variant="outline"
|
|
6155
|
+
role="combobox"
|
|
6156
|
+
aria-expanded={memberPickerOpen}
|
|
6157
|
+
className="w-full justify-between font-normal"
|
|
6158
|
+
>
|
|
6159
|
+
<span className="text-muted-foreground">
|
|
6160
|
+
Search by name or ID...
|
|
6161
|
+
</span>
|
|
6162
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
6163
|
+
</Button>
|
|
6164
|
+
</PopoverTrigger>
|
|
6165
|
+
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
|
6166
|
+
<Command>
|
|
6167
|
+
<CommandInput placeholder="Search members..." />
|
|
6168
|
+
<CommandList>
|
|
6169
|
+
<CommandEmpty>No members found.</CommandEmpty>
|
|
6170
|
+
<CommandGroup>
|
|
6171
|
+
{members?.map((member) => {
|
|
6172
|
+
const checked = selectedMembers.includes(
|
|
6173
|
+
member.discordUserId,
|
|
6174
|
+
);
|
|
6175
|
+
const url = avatarUrl(
|
|
6176
|
+
member.discordUserId,
|
|
6177
|
+
member.avatar,
|
|
6178
|
+
);
|
|
6179
|
+
return (
|
|
6180
|
+
<CommandItem
|
|
6181
|
+
key={member.discordUserId}
|
|
6182
|
+
value={\`\${member.discordUserId} \${member.username} \${member.displayName ?? ""} \${member.nick ?? ""}\`}
|
|
6183
|
+
onSelect={() => toggleMember(member.discordUserId)}
|
|
6184
|
+
>
|
|
6185
|
+
{url ? (
|
|
6186
|
+
<img
|
|
6187
|
+
src={url}
|
|
6188
|
+
alt=""
|
|
6189
|
+
className="mr-2 h-5 w-5 rounded-full shrink-0 object-cover"
|
|
6190
|
+
/>
|
|
6191
|
+
) : (
|
|
6192
|
+
<div className="mr-2 h-5 w-5 rounded-full bg-muted flex items-center justify-center shrink-0">
|
|
6193
|
+
<User className="h-3 w-3 text-muted-foreground" />
|
|
6194
|
+
</div>
|
|
6195
|
+
)}
|
|
6196
|
+
<span className="truncate">
|
|
6197
|
+
{member.displayName ?? member.username}
|
|
6198
|
+
</span>
|
|
6199
|
+
<Check
|
|
6200
|
+
className={cn(
|
|
6201
|
+
"ml-auto h-4 w-4",
|
|
6202
|
+
checked ? "opacity-100" : "opacity-0",
|
|
6203
|
+
)}
|
|
6204
|
+
/>
|
|
6205
|
+
</CommandItem>
|
|
6206
|
+
);
|
|
6207
|
+
})}
|
|
6208
|
+
</CommandGroup>
|
|
6209
|
+
</CommandList>
|
|
6210
|
+
</Command>
|
|
6211
|
+
</PopoverContent>
|
|
6212
|
+
</Popover>
|
|
6213
|
+
</fieldset>
|
|
6214
|
+
|
|
6215
|
+
{/* ROLE POSITION ------------------------------------------------ */}
|
|
6216
|
+
<fieldset>
|
|
6217
|
+
<div className="flex items-center justify-between mb-2">
|
|
6218
|
+
<legend className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
6219
|
+
Role Position
|
|
6220
|
+
</legend>
|
|
6221
|
+
<div className="flex items-center gap-0.5">
|
|
6222
|
+
<button
|
|
6223
|
+
onClick={moveUp}
|
|
6224
|
+
disabled={positionIndex === 0}
|
|
6225
|
+
className="rounded-md p-1 hover:bg-accent disabled:opacity-25 disabled:pointer-events-none transition-colors"
|
|
6226
|
+
title="Move up"
|
|
6227
|
+
>
|
|
6228
|
+
<ChevronUp className="h-4 w-4" />
|
|
6229
|
+
</button>
|
|
6230
|
+
<button
|
|
6231
|
+
onClick={moveDown}
|
|
6232
|
+
disabled={positionIndex >= sortedRoles.length}
|
|
6233
|
+
className="rounded-md p-1 hover:bg-accent disabled:opacity-25 disabled:pointer-events-none transition-colors"
|
|
6234
|
+
title="Move down"
|
|
6235
|
+
>
|
|
6236
|
+
<ChevronDown className="h-4 w-4" />
|
|
6237
|
+
</button>
|
|
6238
|
+
</div>
|
|
6239
|
+
</div>
|
|
6240
|
+
|
|
6241
|
+
<div className="max-h-[200px] overflow-y-auto rounded-md border border-input">
|
|
6242
|
+
{sortedRoles.length === 0 && roles !== undefined ? (
|
|
6243
|
+
<>
|
|
6244
|
+
<NewRoleEntry
|
|
6245
|
+
name={name}
|
|
6246
|
+
color={color}
|
|
6247
|
+
/>
|
|
6248
|
+
<EveryoneEntry />
|
|
6249
|
+
</>
|
|
6250
|
+
) : sortedRoles.length === 0 ? (
|
|
6251
|
+
<div className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
6252
|
+
Loading roles...
|
|
6253
|
+
</div>
|
|
6254
|
+
) : (
|
|
6255
|
+
<>
|
|
6256
|
+
{sortedRoles.map((role, i) => (
|
|
6257
|
+
<div key={role.discordId}>
|
|
6258
|
+
{positionIndex === i && (
|
|
6259
|
+
<NewRoleEntry
|
|
6260
|
+
name={name}
|
|
6261
|
+
color={color}
|
|
6262
|
+
/>
|
|
6263
|
+
)}
|
|
6264
|
+
<div className="flex items-center gap-2.5 px-3 py-2 text-sm">
|
|
6265
|
+
<span
|
|
6266
|
+
className="h-3 w-3 rounded-full shrink-0"
|
|
6267
|
+
style={{ backgroundColor: colorToHex(role.color) }}
|
|
6268
|
+
/>
|
|
6269
|
+
<span className="truncate flex-1">{role.name}</span>
|
|
6270
|
+
<span className="text-[11px] tabular-nums text-muted-foreground">
|
|
6271
|
+
{role.position}
|
|
6272
|
+
</span>
|
|
6273
|
+
</div>
|
|
6274
|
+
</div>
|
|
6275
|
+
))}
|
|
6276
|
+
{positionIndex >= sortedRoles.length && (
|
|
6277
|
+
<NewRoleEntry
|
|
6278
|
+
name={name}
|
|
6279
|
+
color={color}
|
|
6280
|
+
/>
|
|
6281
|
+
)}
|
|
6282
|
+
<EveryoneEntry />
|
|
6283
|
+
</>
|
|
6284
|
+
)}
|
|
6285
|
+
</div>
|
|
6286
|
+
</fieldset>
|
|
6287
|
+
</div>
|
|
6288
|
+
|
|
6289
|
+
{/* ── Footer ──────────────────────────────────────────────── */}
|
|
6290
|
+
<div className="flex items-center justify-end gap-3 border-t px-6 py-4 mt-1">
|
|
6291
|
+
<Button variant="ghost" onClick={() => setOpen(false)}>
|
|
6292
|
+
Cancel
|
|
6293
|
+
</Button>
|
|
6294
|
+
<Button
|
|
6295
|
+
onClick={handleCreate}
|
|
6296
|
+
disabled={!name.trim()}
|
|
6297
|
+
className="transition-colors duration-200"
|
|
6298
|
+
style={
|
|
6299
|
+
color !== 0
|
|
6300
|
+
? { backgroundColor: colorToHex(color), color: "#fff" }
|
|
6301
|
+
: undefined
|
|
6302
|
+
}
|
|
6303
|
+
>
|
|
6304
|
+
Create Role
|
|
6305
|
+
</Button>
|
|
6306
|
+
</div>
|
|
6307
|
+
</DialogContent>
|
|
6308
|
+
</Dialog>
|
|
6309
|
+
);
|
|
6310
|
+
}
|
|
6311
|
+
|
|
6312
|
+
// ── Sub-components ─────────────────────────────────────────────────────────
|
|
6313
|
+
|
|
6314
|
+
function NewRoleEntry({ name, color }: { name: string; color: number }) {
|
|
6315
|
+
return (
|
|
6316
|
+
<div className="flex items-center gap-2.5 px-3 py-2 bg-primary/[0.08] border-l-2 border-l-primary">
|
|
6317
|
+
<span
|
|
6318
|
+
className="h-3 w-3 rounded-full shrink-0 transition-colors duration-200"
|
|
6319
|
+
style={{ backgroundColor: colorToHex(color) }}
|
|
6320
|
+
/>
|
|
6321
|
+
<span className="text-sm font-medium truncate flex-1">
|
|
6322
|
+
{name || "new role"}
|
|
6323
|
+
</span>
|
|
6324
|
+
<span className="text-[11px] font-semibold text-primary">NEW</span>
|
|
6325
|
+
</div>
|
|
6326
|
+
);
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6329
|
+
function EveryoneEntry() {
|
|
6330
|
+
return (
|
|
6331
|
+
<div className="flex items-center gap-2.5 px-3 py-2 opacity-40">
|
|
6332
|
+
<span className="h-3 w-3 rounded-full shrink-0 bg-muted-foreground/50" />
|
|
6333
|
+
<span className="text-sm truncate">@everyone</span>
|
|
6334
|
+
<span className="ml-auto text-[11px] tabular-nums">0</span>
|
|
6335
|
+
</div>
|
|
6336
|
+
);
|
|
6337
|
+
}
|
|
6338
|
+
`,
|
|
6339
|
+
generateConvexQueries: () => `export const listRolesForCreator = query({
|
|
6340
|
+
args: { guildDiscordId: v.string() },
|
|
6341
|
+
returns: v.array(
|
|
6342
|
+
v.object({
|
|
6343
|
+
discordId: v.string(),
|
|
6344
|
+
name: v.string(),
|
|
6345
|
+
color: v.number(),
|
|
6346
|
+
position: v.number(),
|
|
6347
|
+
})
|
|
6348
|
+
),
|
|
6349
|
+
handler: async (ctx, { guildDiscordId }) => {
|
|
6350
|
+
const roles = await ctx.db
|
|
6351
|
+
.query("calabasasRoles")
|
|
6352
|
+
.withIndex("by_guild", (q) => q.eq("guildDiscordId", guildDiscordId))
|
|
6353
|
+
.collect();
|
|
6354
|
+
return roles.map((r) => ({
|
|
6355
|
+
discordId: r.discordId,
|
|
6356
|
+
name: r.name,
|
|
6357
|
+
color: r.color,
|
|
6358
|
+
position: r.position,
|
|
6359
|
+
}));
|
|
6360
|
+
},
|
|
6361
|
+
});
|
|
6362
|
+
|
|
6363
|
+
export const listMembersForCreator = query({
|
|
6364
|
+
args: { guildDiscordId: v.string() },
|
|
6365
|
+
returns: v.array(
|
|
6366
|
+
v.object({
|
|
6367
|
+
discordUserId: v.string(),
|
|
6368
|
+
username: v.string(),
|
|
6369
|
+
displayName: v.optional(v.string()),
|
|
6370
|
+
avatar: v.optional(v.string()),
|
|
6371
|
+
nick: v.optional(v.string()),
|
|
6372
|
+
})
|
|
6373
|
+
),
|
|
6374
|
+
handler: async (ctx, { guildDiscordId }) => {
|
|
6375
|
+
const members = await ctx.db
|
|
6376
|
+
.query("calabasasMembers")
|
|
6377
|
+
.withIndex("by_guild", (q) => q.eq("guildDiscordId", guildDiscordId))
|
|
6378
|
+
.collect();
|
|
6379
|
+
return members.map((m) => ({
|
|
6380
|
+
discordUserId: m.discordUserId,
|
|
6381
|
+
username: m.username,
|
|
6382
|
+
displayName: m.displayName,
|
|
6383
|
+
avatar: m.avatar,
|
|
6384
|
+
nick: m.nick,
|
|
6385
|
+
}));
|
|
6386
|
+
},
|
|
6387
|
+
});`
|
|
6388
|
+
};
|
|
6389
|
+
|
|
5744
6390
|
// src/lib/registry/index.ts
|
|
5745
6391
|
var REGISTRY = [
|
|
5746
6392
|
channelSelect,
|
|
@@ -5754,7 +6400,8 @@ var REGISTRY = [
|
|
|
5754
6400
|
channelTree,
|
|
5755
6401
|
memberRoster,
|
|
5756
6402
|
useOnlineCount,
|
|
5757
|
-
emojiPicker
|
|
6403
|
+
emojiPicker,
|
|
6404
|
+
roleCreator
|
|
5758
6405
|
];
|
|
5759
6406
|
function getComponent(name) {
|
|
5760
6407
|
return REGISTRY.find((c) => c.name === name);
|