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.
Files changed (2) hide show
  1. package/dist/index.js +648 -1
  2. 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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calabasas",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "CLI for Calabasas - Discord Gateway as a Service for Convex",
5
5
  "type": "module",
6
6
  "bin": {