Repository: hypha-dao/hypha-web
Pull request #717: feat(#694): tags for spaces
Author: login: plitzenberger
Created: 2025-04-10T12:38:47Z | Merged: 2025-04-11T07:49:47Z

fix #694

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a category selection option in the space creation form for tagging spaces with multiple categories.
  - Introduced an intuitive multi-select component for streamlined multi-option selection.
  - Rolled out a new set of interactive UI elements—including a command interface, dialog, and popover—to enhance navigation and user interactions.
  - Expanded data categorization support, facilitating improved organization of spaces and related items.
  - Implemented functionality to display spaces categorized by unique categories, including a separate display for uncategorized spaces.
  - Added new dependencies to enhance functionality and performance.

- **Bug Fixes**
  - Updated text styling in the SpaceGroupSlider component for improved readability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #694: Tags in Network view
In the absence of an ecosystem view in mvp, I'm proposing a very simple workaround that will fulfill immediate needs

In the "Space Settings > Configure Space, can we please add a tag field. That would such a BIG WIN!

Impact Focus: Energy, Water, Soil etc....

https://docs.google.com/presentation/d/1LfOhBMy5QNuMX0QLFs2eECdvwTe1jE5N_u20ZHYZQ0A/edit?slide=id.g2a67fa6f9a1_0_2#slide=id.g2a67fa6f9a1_0_2

<img width="889" alt="Image" src="https://github.com/user-attachments/assets/564d510a-d1d6-4364-8b2a-7ff0400d7c1c" />
Linked issue comments: user: login: alexprate
created_at: 2025-04-07T20:50:54Z
body: @plitzenberger Here is the slide from David which is interesting when it comes to defining the categories in the Network page. let's enable tags if possible and then discuss which ones will be default

With David's comment: I would probably expand “green” to soil, flora, fauna, fungi

<img width="966" alt="Image" src="https://github.com/user-attachments/assets/c79d5414-3017-404e-a2f4-efa9a5cc42b1" />
author_association: CONTRIBUTOR
user: login: plitzenberger
created_at: 2025-04-09T12:03:02Z
body: @alexprate shouldn't we include this tag at space creation already?
author_association: MEMBER
user: login: alexprate
created_at: 2025-04-09T17:32:38Z
body: > [@alexprate](https://github.com/alexprate) shouldn't we include this tag at space creation already?

Yes @plitzenberger  that is exactly what I'm proposing to add a tag field in the "Create Space" screen
author_association: CONTRIBUTOR

Diff:
diff --git a/apps/web/src/app/[lang]/network/page.tsx b/apps/web/src/app/[lang]/network/page.tsx
index ed4bf1d7fe..5b5170a6b9 100644
--- a/apps/web/src/app/[lang]/network/page.tsx
+++ b/apps/web/src/app/[lang]/network/page.tsx
@@ -3,12 +3,25 @@ import { Container } from '@hypha-platform/ui';
 import { Text } from '@radix-ui/themes';
 import { SpaceGroupSlider, SpaceSearch } from '@hypha-platform/epics';
 import { getDhoPathAgreements } from '../dho/[id]/agreements/constants';
-import { createSpaceService } from '@hypha-platform/core/server';
+import { createSpaceService, Space } from '@hypha-platform/core/server';
+import { Category } from '@hypha-platform/core/client';
 
 type PageProps = {
   params: Promise<{ lang: Locale; id: string }>;
 };
 
+function extractUniqueCategories(spaces: Space[]): Category[] {
+  const categoriesSet = new Set<Category>();
+
+  spaces.forEach((space) => {
+    if (space.categories) {
+      space.categories.forEach((category) => categoriesSet.add(category));
+    }
+  });
+
+  return Array.from(categoriesSet);
+}
+
 export default async function Index(props: PageProps) {
   const params = await props.params;
 
@@ -19,6 +32,7 @@ export default async function Index(props: PageProps) {
   };
 
   const spaces = await createSpaceService().getAll();
+  const uniqueCategories = extractUniqueCategories(spaces);
 
   return (
     <Container>
@@ -26,7 +40,21 @@ export default async function Index(props: PageProps) {
         Explore hundreds of Spaces in the Hypha Network
       </Text>
       <SpaceSearch />
-      <SpaceGroupSlider spaces={spaces} type="Hypha" getHref={getHref} />
+      {uniqueCategories.map((category) => (
+        <SpaceGroupSlider
+          key={category}
+          spaces={spaces.filter((space) =>
+            space.categories?.includes(category),
+          )}
+          type={category}
+          getHref={getHref}
+        />
+      ))}
+      <SpaceGroupSlider
+        spaces={spaces.filter((space) => space.categories?.length === 0)}
+        type={'No Category'}
+        getHref={getHref}
+      />
     </Container>
   );
 }
diff --git a/package.json b/package.json
index f91204245b..9cd5060c02 100644
--- a/package.json
+++ b/package.json
@@ -32,10 +32,11 @@
     "@privy-io/wagmi": "^1.0.3",
     "@radix-ui/react-aspect-ratio": "^1.1.2",
     "@radix-ui/react-avatar": "^1.1.2",
-    "@radix-ui/react-dialog": "^1.1.4",
+    "@radix-ui/react-dialog": "^1.1.6",
     "@radix-ui/react-dropdown-menu": "^2.1.4",
     "@radix-ui/react-icons": "^1.3.2",
     "@radix-ui/react-label": "^2.1.2",
+    "@radix-ui/react-popover": "^1.1.7",
     "@radix-ui/react-progress": "^1.1.2",
     "@radix-ui/react-scroll-area": "^1.2.2",
     "@radix-ui/react-select": "^2.1.6",
@@ -65,6 +66,7 @@
     "@xstate/store": "^3.2.1",
     "class-variance-authority": "^0.7.0",
     "clsx": "^2.1.1",
+    "cmdk": "^1.1.1",
     "commander": "^13.1.0",
     "date-fns": "^4.1.0",
     "drizzle-orm": "^0.38.4",
diff --git a/packages/core/src/categories/constants.ts b/packages/core/src/categories/constants.ts
new file mode 100644
index 0000000000..da454b0044
--- /dev/null
+++ b/packages/core/src/categories/constants.ts
@@ -0,0 +1,16 @@
+export const categories = [
+  { value: 'housing', label: 'Housing' },
+  { value: 'energy', label: 'Energy' },
+  { value: 'mobility', label: 'Mobility' },
+  { value: 'water', label: 'Water' },
+  { value: 'air', label: 'Air' },
+  { value: 'soil', label: 'Soil' },
+  { value: 'flora', label: 'Flora' },
+  { value: 'fauna', label: 'Fauna' },
+  { value: 'fungi', label: 'Fungi' },
+  { value: 'food', label: 'Food' },
+  { value: 'education', label: 'Education' },
+  { value: 'art', label: 'Art' },
+  { value: 'health', label: 'Health' },
+  { value: 'tech', label: 'Tech' },
+];
diff --git a/packages/core/src/categories/index.ts b/packages/core/src/categories/index.ts
new file mode 100644
index 0000000000..4fbce9564d
--- /dev/null
+++ b/packages/core/src/categories/index.ts
@@ -0,0 +1,2 @@
+export * from './constants';
+export * from './types';
diff --git a/packages/core/src/categories/types.ts b/packages/core/src/categories/types.ts
new file mode 100644
index 0000000000..72b916eb78
--- /dev/null
+++ b/packages/core/src/categories/types.ts
@@ -0,0 +1,15 @@
+export type Category =
+  | 'housing'
+  | 'energy'
+  | 'mobility'
+  | 'water'
+  | 'air'
+  | 'soil'
+  | 'flora'
+  | 'fauna'
+  | 'fungi'
+  | 'food'
+  | 'education'
+  | 'art'
+  | 'health'
+  | 'tech';
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 7a65112540..9647ce1e90 100644
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -1,3 +1,4 @@
+export * from './categories';
 export * from './common';
 export * from './governance';
 export * from './people';
diff --git a/packages/core/src/space/types.ts b/packages/core/src/space/types.ts
index e1bfb3d1ea..6184b57ba3 100644
--- a/packages/core/src/space/types.ts
+++ b/packages/core/src/space/types.ts
@@ -1,3 +1,5 @@
+import { Category } from '@core/categories/types';
+
 export interface Space {
   id: number;
   logoUrl?: string | null;
@@ -8,6 +10,7 @@ export interface Space {
   parentId?: number | null;
   web3SpaceId?: number | null;
   links?: string[] | null;
+  categories?: Category[] | null;
 }
 
 export interface CreateSpaceInput {
@@ -18,6 +21,7 @@ export interface CreateSpaceInput {
   slug?: string;
   parentId?: number;
   links?: string[];
+  categories?: Category[];
 }
 
 export interface UpdateSpaceInput {
diff --git a/packages/core/src/space/validation.ts b/packages/core/src/space/validation.ts
index 73fbedb6fc..dc13e6b7e1 100644
--- a/packages/core/src/space/validation.ts
+++ b/packages/core/src/space/validation.ts
@@ -17,6 +17,26 @@ const createSpaceWeb2Props = {
     .optional(),
   web3SpaceId: z.number().optional(),
   parentId: z.number().optional(),
+  categories: z
+    .array(
+      z.enum([
+        'housing',
+        'energy',
+        'mobility',
+        'water',
+        'air',
+        'soil',
+        'flora',
+        'fauna',
+        'fungi',
+        'food',
+        'education',
+        'art',
+        'health',
+        'tech',
+      ]),
+    )
+    .default([]),
   links: z
     .array(z.string().url('Links must be a valid URL'))
     .max(3)
diff --git a/packages/epics/src/spaces/components/create-space-form.tsx b/packages/epics/src/spaces/components/create-space-form.tsx
index 40d81fb2ce..59c114556d 100644
--- a/packages/epics/src/spaces/components/create-space-form.tsx
+++ b/packages/epics/src/spaces/components/create-space-form.tsx
@@ -14,6 +14,7 @@ import {
   FormMessage,
   UploadAvatar,
   UploadLeadImage,
+  MultiSelect,
 } from '@hypha-platform/ui';
 import { RxCross1 } from 'react-icons/rx';
 import { Text } from '@radix-ui/themes';
@@ -24,11 +25,11 @@ import { z } from 'zod';
 import clsx from 'clsx';
 import {
   ALLOWED_IMAGE_FILE_SIZE,
+  categories,
   createSpaceFiles,
   schemaCreateSpace,
 } from '@hypha-platform/core/client';
 import { Links } from '../../common/links';
-import React from 'react';
 
 const schemaCreateSpaceForm = schemaCreateSpace.extend(createSpaceFiles);
 
@@ -61,6 +62,7 @@ export const CreateSpaceForm = ({
       exitMethod: 0,
       logoUrl: undefined,
       leadImage: undefined,
+      categories: [],
       links: [],
     },
   });
@@ -161,6 +163,24 @@ export const CreateSpaceForm = ({
             </FormItem>
           )}
         />
+        <FormField
+          control={form.control}
+          name="categories"
+          render={({ field }) => (
+            <FormItem>
+              <FormLabel>Tags</FormLabel>
+              <FormControl>
+                <MultiSelect
+                  placeholder={'Select one or more'}
+                  options={categories}
+                  defaultValue={field.value}
+                  onValueChange={field.onChange}
+                />
+              </FormControl>
+              <FormMessage />
+            </FormItem>
+          )}
+        />
         <FormField
           control={form.control}
           name="links"
diff --git a/packages/epics/src/spaces/components/space-group-slider.tsx b/packages/epics/src/spaces/components/space-group-slider.tsx
index 58cd22127b..11d0f7a0ba 100644
--- a/packages/epics/src/spaces/components/space-group-slider.tsx
+++ b/packages/epics/src/spaces/components/space-group-slider.tsx
@@ -26,7 +26,7 @@ export const SpaceGroupSlider = ({
   return (
     <div className="border-b-2 border-primary-foreground pt-6">
       <div className="flex justify-between items-center">
-        <Text className="text-4 font-medium">
+        <Text className="text-4 font-medium capitalize">
           {type} | {spaces?.length}
         </Text>
         <Button variant="ghost" className="text-accent-11">
diff --git a/packages/storage-postgres/migrations/0020_wide_johnny_blaze.sql b/packages/storage-postgres/migrations/0020_wide_johnny_blaze.sql
new file mode 100644
index 0000000000..dead7ae9c3
--- /dev/null
+++ b/packages/storage-postgres/migrations/0020_wide_johnny_blaze.sql
@@ -0,0 +1 @@
+ALTER TABLE "spaces" ADD COLUMN "categories" jsonb DEFAULT '[]'::jsonb;
\ No newline at end of file
diff --git a/packages/storage-postgres/migrations/meta/0020_snapshot.json b/packages/storage-postgres/migrations/meta/0020_snapshot.json
new file mode 100644
index 0000000000..9f0aa8a8fa
--- /dev/null
+++ b/packages/storage-postgres/migrations/meta/0020_snapshot.json
@@ -0,0 +1,466 @@
+{
+  "id": "acf1ea6a-a2ab-40c1-b3b8-4b3e68e22532",
+  "prevId": "61cc4ede-5bf8-4cea-830e-6865a9cbbe46",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.documents": {
+      "name": "documents",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "creator_id": {
+          "name": "creator_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "space_id": {
+          "name": "space_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "state": {
+          "name": "state",
+          "type": "document_state",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'discussion'"
+        },
+        "slug": {
+          "name": "slug",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "documents_creator_id_people_id_fk": {
+          "name": "documents_creator_id_people_id_fk",
+          "tableFrom": "documents",
+          "tableTo": "people",
+          "columnsFrom": [
+            "creator_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "documents_space_id_spaces_id_fk": {
+          "name": "documents_space_id_spaces_id_fk",
+          "tableFrom": "documents",
+          "tableTo": "spaces",
+          "columnsFrom": [
+            "space_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.memberships": {
+      "name": "memberships",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "person_id": {
+          "name": "person_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "space_id": {
+          "name": "space_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "person_space_idx": {
+          "name": "person_space_idx",
+          "columns": [
+            {
+              "expression": "person_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "space_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "memberships_person_id_people_id_fk": {
+          "name": "memberships_person_id_people_id_fk",
+          "tableFrom": "memberships",
+          "tableTo": "people",
+          "columnsFrom": [
+            "person_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "memberships_space_id_spaces_id_fk": {
+          "name": "memberships_space_id_spaces_id_fk",
+          "tableFrom": "memberships",
+          "tableTo": "spaces",
+          "columnsFrom": [
+            "space_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.people": {
+      "name": "people",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "sub": {
+          "name": "sub",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "(auth.user_id())"
+        },
+        "slug": {
+          "name": "slug",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "avatar_url": {
+          "name": "avatar_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "lead_image_url": {
+          "name": "lead_image_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "email": {
+          "name": "email",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "location": {
+          "name": "location",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "surname": {
+          "name": "surname",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "nickname": {
+          "name": "nickname",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "people_sub_unique": {
+          "name": "people_sub_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "sub"
+          ]
+        },
+        "people_slug_unique": {
+          "name": "people_slug_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "slug"
+          ]
+        },
+        "people_email_unique": {
+          "name": "people_email_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "email"
+          ]
+        }
+      },
+      "policies": {
+        "crud-authenticated-policy-select": {
+          "name": "crud-authenticated-policy-select",
+          "as": "PERMISSIVE",
+          "for": "SELECT",
+          "to": [
+            "authenticated"
+          ],
+          "using": "true"
+        },
+        "crud-authenticated-policy-insert": {
+          "name": "crud-authenticated-policy-insert",
+          "as": "PERMISSIVE",
+          "for": "INSERT",
+          "to": [
+            "authenticated"
+          ],
+          "withCheck": "(select auth.user_id() = \"people\".\"sub\")"
+        },
+        "crud-authenticated-policy-update": {
+          "name": "crud-authenticated-policy-update",
+          "as": "PERMISSIVE",
+          "for": "UPDATE",
+          "to": [
+            "authenticated"
+          ],
+          "using": "(select auth.user_id() = \"people\".\"sub\")",
+          "withCheck": "(select auth.user_id() = \"people\".\"sub\")"
+        },
+        "crud-authenticated-policy-delete": {
+          "name": "crud-authenticated-policy-delete",
+          "as": "PERMISSIVE",
+          "for": "DELETE",
+          "to": [
+            "authenticated"
+          ],
+          "using": "(select auth.user_id() = \"people\".\"sub\")"
+        }
+      },
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.spaces": {
+      "name": "spaces",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "logo_url": {
+          "name": "logo_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "lead_image": {
+          "name": "lead_image",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "slug": {
+          "name": "slug",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "web3_space_id": {
+          "name": "web3_space_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "links": {
+          "name": "links",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "categories": {
+          "name": "categories",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "spaces_slug_unique": {
+          "name": "spaces_slug_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "slug"
+          ]
+        }
+      },
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {
+    "public.document_state": {
+      "name": "document_state",
+      "schema": "public",
+      "values": [
+        "discussion",
+        "proposal",
+        "agreement"
+      ]
+    }
+  },
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}
\ No newline at end of file
diff --git a/packages/storage-postgres/migrations/meta/_journal.json b/packages/storage-postgres/migrations/meta/_journal.json
index bf09d95ba8..129bcdbd69 100644
--- a/packages/storage-postgres/migrations/meta/_journal.json
+++ b/packages/storage-postgres/migrations/meta/_journal.json
@@ -141,6 +141,13 @@
       "when": 1744183962326,
       "tag": "0019_sour_micromacro",
       "breakpoints": true
+    },
+    {
+      "idx": 20,
+      "version": "7",
+      "when": 1744285062588,
+      "tag": "0020_wide_johnny_blaze",
+      "breakpoints": true
     }
   ]
 }
\ No newline at end of file
diff --git a/packages/storage-postgres/src/schema/categories.ts b/packages/storage-postgres/src/schema/categories.ts
new file mode 100644
index 0000000000..543bc9a61e
--- /dev/null
+++ b/packages/storage-postgres/src/schema/categories.ts
@@ -0,0 +1,20 @@
+import { pgEnum } from 'drizzle-orm/pg-core';
+
+export const CATEGORIES = [
+  'housing',
+  'energy',
+  'mobility',
+  'water',
+  'air',
+  'soil',
+  'flora',
+  'fauna',
+  'fungi',
+  'food',
+  'education',
+  'art',
+  'health',
+  'tech',
+] as const;
+
+export const categories = pgEnum('categories', CATEGORIES);
diff --git a/packages/storage-postgres/src/schema/space.ts b/packages/storage-postgres/src/schema/space.ts
index 616c9f5c89..2e8cfe45d2 100644
--- a/packages/storage-postgres/src/schema/space.ts
+++ b/packages/storage-postgres/src/schema/space.ts
@@ -1,6 +1,7 @@
 import { InferInsertModel, InferSelectModel } from 'drizzle-orm';
 import { integer, jsonb, pgTable, serial, text } from 'drizzle-orm/pg-core';
 import { commonDateFields } from './shared';
+import { categories } from './categories';
 
 export const spaces = pgTable('spaces', {
   id: serial('id').primaryKey(),
@@ -11,6 +12,9 @@ export const spaces = pgTable('spaces', {
   slug: text('slug').notNull().unique(),
   web3SpaceId: integer('web3_space_id'),
   links: jsonb('links').$type<string[]>().default([]),
+  categories: jsonb('categories')
+    .$type<Array<(typeof categories.enumValues)[number]>>()
+    .default([]),
   ...commonDateFields,
 });
 
diff --git a/packages/storage-postgres/src/seed.ts b/packages/storage-postgres/src/seed.ts
index 23ae9ba0e1..1d8011692c 100644
--- a/packages/storage-postgres/src/seed.ts
+++ b/packages/storage-postgres/src/seed.ts
@@ -4,6 +4,7 @@ import { seed, reset } from 'drizzle-seed';
 
 import { resetIndexes } from './utils/reset-index';
 import { documents, memberships, people, schema, spaces } from './schema';
+import { CATEGORIES } from './schema/categories';
 
 const AVATAR_URLS = Array.from({ length: 10 }, () => faker.image.avatar());
 const SPACE_LOGO_URLS = Array.from({ length: 10 }, () =>
@@ -19,6 +20,9 @@ const SPACE_LEAD_IMAGE_URLS = Array.from({ length: 10 }, () =>
   }),
 );
 
+const SAMPLE_LINKS = Array.from({ length: 10 }, () => faker.internet.url());
+const SAMPLE_CATEGORIES = [...CATEGORIES];
+
 async function main() {
   const db = drizzle(process.env.BRANCH_DB_URL! || process.env.DEFAULT_DB_URL!);
   await reset(db, { people, memberships, spaces, documents });
@@ -64,6 +68,14 @@ async function main() {
           leadImage: f.valuesFromArray({
             values: SPACE_LEAD_IMAGE_URLS,
           }),
+          links: f.valuesFromArray({
+            values: SAMPLE_LINKS,
+            arraySize: 2,
+          }),
+          categories: f.valuesFromArray({
+            values: SAMPLE_CATEGORIES,
+            arraySize: 2,
+          }),
         },
       },
     };
diff --git a/packages/ui/src/chips.stories.tsx b/packages/ui/src/chips.stories.tsx
new file mode 100644
index 0000000000..7489220f31
--- /dev/null
+++ b/packages/ui/src/chips.stories.tsx
@@ -0,0 +1,45 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { within } from '@storybook/testing-library';
+import { expect } from '@storybook/jest';
+
+import { MultiSelect } from './chips';
+
+const frameworksList = [
+  { value: 'housing', label: 'Housing' },
+  { value: 'energy', label: 'Energy' },
+  { value: 'mobility', label: 'Mobility' },
+  { value: 'water', label: 'Water' },
+  { value: 'air', label: 'Air' },
+  { value: 'soil', label: 'Soil' },
+  { value: 'flora', label: 'Flora' },
+  { value: 'fauna', label: 'Fauna' },
+  { value: 'fungi', label: 'Fungi' },
+  { value: 'food', label: 'Food' },
+  { value: 'education', label: 'Education' },
+  { value: 'art', label: 'Art' },
+  { value: 'health', label: 'Health' },
+  { value: 'tech', label: 'Tech' },
+];
+
+const meta: Meta<typeof MultiSelect> = {
+  component: MultiSelect,
+  title: 'UI/MultiSelect',
+};
+export default meta;
+type Story = StoryObj<typeof MultiSelect>;
+
+export const Primary: Story = {
+  args: {
+    options: frameworksList,
+    defaultValue: [],
+    placeholder: 'Select Categories',
+    variant: 'inverted',
+    animation: 2,
+    maxCount: 3,
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    expect(canvas.getByText(/Select Categories/gi)).toBeTruthy();
+  },
+};
diff --git a/packages/ui/src/chips.tsx b/packages/ui/src/chips.tsx
new file mode 100644
index 0000000000..bb175490c8
--- /dev/null
+++ b/packages/ui/src/chips.tsx
@@ -0,0 +1,354 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { CheckIcon, XCircle, ChevronDown, XIcon } from 'lucide-react';
+import { Popover, PopoverContent, PopoverTrigger } from './popover';
+import { Button } from './button';
+import { cn } from '@hypha-platform/ui-utils';
+import { Badge } from './badge';
+import { Separator } from './separator';
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+  CommandSeparator,
+} from './command';
+
+/**
+ * Variants for the multi-select component to handle different styles.
+ * Uses class-variance-authority (cva) to define different styles based on "variant" prop.
+ */
+const multiSelectVariants = cva(
+  'm-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300',
+  {
+    variants: {
+      variant: {
+        default:
+          'border-foreground/10 text-foreground bg-card hover:bg-card/80',
+        secondary:
+          'border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80',
+        destructive:
+          'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+        inverted: 'inverted',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+);
+
+/**
+ * Props for MultiSelect component
+ */
+interface MultiSelectProps
+  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+    VariantProps<typeof multiSelectVariants> {
+  /**
+   * An array of option objects to be displayed in the multi-select component.
+   * Each option object has a label, value, and an optional icon.
+   */
+  options: {
+    /** The text to display for the option. */
+    label: string;
+    /** The unique value associated with the option. */
+    value: string;
+    /** Optional icon component to display alongside the option. */
+    icon?: React.ComponentType<{ className?: string }>;
+  }[];
+
+  /**
+   * Callback function triggered when the selected values change.
+   * Receives an array of the new selected values.
+   */
+  onValueChange: (value: string[]) => void;
+
+  /** The default selected values when the component mounts. */
+  defaultValue?: string[];
+
+  /**
+   * Placeholder text to be displayed when no values are selected.
+   * Optional, defaults to "Select options".
+   */
+  placeholder?: string;
+
+  /**
+   * Animation duration in seconds for the visual effects (e.g., bouncing badges).
+   * Optional, defaults to 0 (no animation).
+   */
+  animation?: number;
+
+  /**
+   * Maximum number of items to display. Extra selected items will be summarized.
+   * Optional, defaults to 3.
+   */
+  maxCount?: number;
+
+  /**
+   * The modality of the popover. When set to true, interaction with outside elements
+   * will be disabled and only popover content will be visible to screen readers.
+   * Optional, defaults to false.
+   */
+  modalPopover?: boolean;
+
+  /**
+   * If true, renders the multi-select component as a child of another component.
+   * Optional, defaults to false.
+   */
+  asChild?: boolean;
+
+  /**
+   * Additional class names to apply custom styles to the multi-select component.
+   * Optional, can be used to add custom styles.
+   */
+  className?: string;
+}
+
+export const MultiSelect = React.forwardRef<
+  HTMLButtonElement,
+  MultiSelectProps
+>(
+  (
+    {
+      options,
+      onValueChange,
+      variant,
+      defaultValue = [],
+      placeholder = 'Select options',
+      animation = 0,
+      maxCount = 3,
+      modalPopover = false,
+      asChild = false,
+      className,
+      ...props
+    },
+    ref,
+  ) => {
+    const [selectedValues, setSelectedValues] =
+      React.useState<string[]>(defaultValue);
+    const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
+
+    const handleInputKeyDown = (
+      event: React.KeyboardEvent<HTMLInputElement>,
+    ) => {
+      if (event.key === 'Enter') {
+        setIsPopoverOpen(true);
+      } else if (event.key === 'Backspace' && !event.currentTarget.value) {
+        const newSelectedValues = [...selectedValues];
+        newSelectedValues.pop();
+        setSelectedValues(newSelectedValues);
+        onValueChange(newSelectedValues);
+      }
+    };
+
+    const toggleOption = (option: string) => {
+      const newSelectedValues = selectedValues.includes(option)
+        ? selectedValues.filter((value) => value !== option)
+        : [...selectedValues, option];
+      setSelectedValues(newSelectedValues);
+      onValueChange(newSelectedValues);
+    };
+
+    const handleClear = () => {
+      setSelectedValues([]);
+      onValueChange([]);
+    };
+
+    const handleTogglePopover = () => {
+      setIsPopoverOpen((prev) => !prev);
+    };
+
+    const clearExtraOptions = () => {
+      const newSelectedValues = selectedValues.slice(0, maxCount);
+      setSelectedValues(newSelectedValues);
+      onValueChange(newSelectedValues);
+    };
+
+    const toggleAll = () => {
+      if (selectedValues.length === options.length) {
+        handleClear();
+      } else {
+        const allValues = options.map((option) => option.value);
+        setSelectedValues(allValues);
+        onValueChange(allValues);
+      }
+    };
+
+    return (
+      <Popover
+        open={isPopoverOpen}
+        onOpenChange={setIsPopoverOpen}
+        modal={modalPopover}
+      >
+        <PopoverTrigger asChild>
+          <Button
+            ref={ref}
+            {...props}
+            onClick={handleTogglePopover}
+            className={cn(
+              'flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
+              className,
+            )}
+          >
+            {selectedValues.length > 0 ? (
+              <div className="flex justify-between items-center w-full">
+                <div className="flex flex-wrap items-center">
+                  {selectedValues.slice(0, maxCount).map((value) => {
+                    const option = options.find((o) => o.value === value);
+                    const IconComponent = option?.icon;
+                    return (
+                      <Badge
+                        key={value}
+                        className={cn(multiSelectVariants({ variant }))}
+                        style={{ animationDuration: `${animation}s` }}
+                      >
+                        {IconComponent && (
+                          <IconComponent className="h-4 w-4 mr-2" />
+                        )}
+                        {option?.label}
+                        <XCircle
+                          className="ml-2 h-4 w-4 cursor-pointer"
+                          onClick={(event) => {
+                            event.stopPropagation();
+                            toggleOption(value);
+                          }}
+                        />
+                      </Badge>
+                    );
+                  })}
+                  {selectedValues.length > maxCount && (
+                    <Badge
+                      className={cn(
+                        'bg-transparent text-foreground border-foreground/1 hover:bg-transparent',
+                        multiSelectVariants({ variant }),
+                      )}
+                      style={{ animationDuration: `${animation}s` }}
+                    >
+                      {`+ ${selectedValues.length - maxCount} more`}
+                      <XCircle
+                        className="ml-2 h-4 w-4 cursor-pointer"
+                        onClick={(event) => {
+                          event.stopPropagation();
+                          clearExtraOptions();
+                        }}
+                      />
+                    </Badge>
+                  )}
+                </div>
+                <div className="flex items-center justify-between">
+                  <XIcon
+                    className="h-4 mx-2 cursor-pointer text-muted-foreground"
+                    onClick={(event) => {
+                      event.stopPropagation();
+                      handleClear();
+                    }}
+                  />
+                  <Separator
+                    orientation="vertical"
+                    className="flex min-h-6 h-full"
+                  />
+                  <ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
+                </div>
+              </div>
+            ) : (
+              <div className="flex items-center justify-between w-full mx-auto">
+                <span className="text-sm text-muted-foreground mx-3">
+                  {placeholder}
+                </span>
+                <ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
+              </div>
+            )}
+          </Button>
+        </PopoverTrigger>
+        <PopoverContent
+          className="w-auto p-0"
+          align="start"
+          onEscapeKeyDown={() => setIsPopoverOpen(false)}
+        >
+          <Command>
+            <CommandInput
+              placeholder="Search..."
+              onKeyDown={handleInputKeyDown}
+            />
+            <CommandList>
+              <CommandEmpty>No results found.</CommandEmpty>
+              <CommandGroup>
+                <CommandItem
+                  key="all"
+                  onSelect={toggleAll}
+                  className="cursor-pointer"
+                >
+                  <div
+                    className={cn(
+                      'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
+                      selectedValues.length === options.length
+                        ? 'bg-primary text-primary-foreground'
+                        : 'opacity-50 [&_svg]:invisible',
+                    )}
+                  >
+                    <CheckIcon className="h-4 w-4" />
+                  </div>
+                  <span>(Select All)</span>
+                </CommandItem>
+                {options.map((option) => {
+                  const isSelected = selectedValues.includes(option.value);
+                  return (
+                    <CommandItem
+                      key={option.value}
+                      onSelect={() => toggleOption(option.value)}
+                      className="cursor-pointer"
+                    >
+                      <div
+                        className={cn(
+                          'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
+                          isSelected
+                            ? 'bg-primary text-primary-foreground'
+                            : 'opacity-50 [&_svg]:invisible',
+                        )}
+                      >
+                        <CheckIcon className="h-4 w-4" />
+                      </div>
+                      {option.icon && (
+                        <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
+                      )}
+                      <span>{option.label}</span>
+                    </CommandItem>
+                  );
+                })}
+              </CommandGroup>
+              <CommandSeparator />
+              <CommandGroup>
+                <div className="flex items-center justify-between">
+                  {selectedValues.length > 0 && (
+                    <>
+                      <CommandItem
+                        onSelect={handleClear}
+                        className="flex-1 justify-center cursor-pointer"
+                      >
+                        Clear
+                      </CommandItem>
+                      <Separator
+                        orientation="vertical"
+                        className="flex min-h-6 h-full"
+                      />
+                    </>
+                  )}
+                  <CommandItem
+                    onSelect={() => setIsPopoverOpen(false)}
+                    className="flex-1 justify-center cursor-pointer max-w-full"
+                  >
+                    Close
+                  </CommandItem>
+                </div>
+              </CommandGroup>
+            </CommandList>
+          </Command>
+        </PopoverContent>
+      </Popover>
+    );
+  },
+);
+
+MultiSelect.displayName = 'MultiSelect';
diff --git a/packages/ui/src/command.tsx b/packages/ui/src/command.tsx
new file mode 100644
index 0000000000..6cdcc8ed06
--- /dev/null
+++ b/packages/ui/src/command.tsx
@@ -0,0 +1,153 @@
+'use client';
+
+import * as React from 'react';
+import { type DialogProps } from '@radix-ui/react-dialog';
+import { Command as CommandPrimitive } from 'cmdk';
+import { Search } from 'lucide-react';
+
+import { cn } from '@hypha-platform/ui-utils';
+import { Dialog, DialogContent } from './dialog';
+
+const Command = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive
+    ref={ref}
+    className={cn(
+      'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
+      className,
+    )}
+    {...props}
+  />
+));
+Command.displayName = CommandPrimitive.displayName;
+
+const CommandDialog = ({ children, ...props }: DialogProps) => {
+  return (
+    <Dialog {...props}>
+      <DialogContent className="overflow-hidden p-0 shadow-lg">
+        <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
+          {children}
+        </Command>
+      </DialogContent>
+    </Dialog>
+  );
+};
+
+const CommandInput = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Input>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
+>(({ className, ...props }, ref) => (
+  <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
+    <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
+    <CommandPrimitive.Input
+      ref={ref}
+      className={cn(
+        'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
+        className,
+      )}
+      {...props}
+    />
+  </div>
+));
+
+CommandInput.displayName = CommandPrimitive.Input.displayName;
+
+const CommandList = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.List>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive.List
+    ref={ref}
+    className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
+    {...props}
+  />
+));
+
+CommandList.displayName = CommandPrimitive.List.displayName;
+
+const CommandEmpty = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Empty>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
+>((props, ref) => (
+  <CommandPrimitive.Empty
+    ref={ref}
+    className="py-6 text-center text-sm"
+    {...props}
+  />
+));
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
+
+const CommandGroup = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Group>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive.Group
+    ref={ref}
+    className={cn(
+      'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
+      className,
+    )}
+    {...props}
+  />
+));
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName;
+
+const CommandSeparator = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Separator>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive.Separator
+    ref={ref}
+    className={cn('-mx-1 h-px bg-border', className)}
+    {...props}
+  />
+));
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
+
+const CommandItem = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Item>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive.Item
+    ref={ref}
+    className={cn(
+      "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+      className,
+    )}
+    {...props}
+  />
+));
+
+CommandItem.displayName = CommandPrimitive.Item.displayName;
+
+const CommandShortcut = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLSpanElement>) => {
+  return (
+    <span
+      className={cn(
+        'ml-auto text-xs tracking-widest text-muted-foreground',
+        className,
+      )}
+      {...props}
+    />
+  );
+};
+CommandShortcut.displayName = 'CommandShortcut';
+
+export {
+  Command,
+  CommandDialog,
+  CommandInput,
+  CommandList,
+  CommandEmpty,
+  CommandGroup,
+  CommandItem,
+  CommandShortcut,
+  CommandSeparator,
+};
diff --git a/packages/ui/src/dialog.tsx b/packages/ui/src/dialog.tsx
new file mode 100644
index 0000000000..87a2b4d285
--- /dev/null
+++ b/packages/ui/src/dialog.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import * as React from 'react';
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import { X } from 'lucide-react';
+
+import { cn } from '@hypha-platform/ui-utils';
+
+const Dialog = DialogPrimitive.Root;
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogClose = DialogPrimitive.Close;
+
+const DialogOverlay = React.forwardRef<
+  React.ElementRef<typeof DialogPrimitive.Overlay>,
+  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
+>(({ className, ...props }, ref) => (
+  <DialogPrimitive.Overlay
+    ref={ref}
+    className={cn(
+      'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
+      className,
+    )}
+    {...props}
+  />
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef<
+  React.ElementRef<typeof DialogPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
+>(({ className, children, ...props }, ref) => (
+  <DialogPortal>
+    <DialogOverlay />
+    <DialogPrimitive.Content
+      ref={ref}
+      className={cn(
+        'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
+        <X className="h-4 w-4" />
+        <span className="sr-only">Close</span>
+      </DialogPrimitive.Close>
+    </DialogPrimitive.Content>
+  </DialogPortal>
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      'flex flex-col space-y-1.5 text-center sm:text-left',
+      className,
+    )}
+    {...props}
+  />
+);
+DialogHeader.displayName = 'DialogHeader';
+
+const DialogFooter = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
+      className,
+    )}
+    {...props}
+  />
+);
+DialogFooter.displayName = 'DialogFooter';
+
+const DialogTitle = React.forwardRef<
+  React.ElementRef<typeof DialogPrimitive.Title>,
+  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
+>(({ className, ...props }, ref) => (
+  <DialogPrimitive.Title
+    ref={ref}
+    className={cn(
+      'text-lg font-semibold leading-none tracking-tight',
+      className,
+    )}
+    {...props}
+  />
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef<
+  React.ElementRef<typeof DialogPrimitive.Description>,
+  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
+>(({ className, ...props }, ref) => (
+  <DialogPrimitive.Description
+    ref={ref}
+    className={cn('text-sm text-muted-foreground', className)}
+    {...props}
+  />
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+  Dialog,
+  DialogPortal,
+  DialogOverlay,
+  DialogClose,
+  DialogTrigger,
+  DialogContent,
+  DialogHeader,
+  DialogFooter,
+  DialogTitle,
+  DialogDescription,
+};
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index cc7726653d..1c5a7039cd 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -7,6 +7,7 @@ export * from './button';
 export * from './card';
 export * from './carousel';
 export * from './chart';
+export * from './chips';
 export * from './container';
 export * from './dropdown-menu';
 export * from './filter-menu';
diff --git a/packages/ui/src/popover.tsx b/packages/ui/src/popover.tsx
new file mode 100644
index 0000000000..35f963ad78
--- /dev/null
+++ b/packages/ui/src/popover.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import * as React from 'react';
+import * as PopoverPrimitive from '@radix-ui/react-popover';
+
+import { cn } from '@hypha-platform/ui-utils';
+
+const Popover = PopoverPrimitive.Root;
+
+const PopoverTrigger = PopoverPrimitive.Trigger;
+
+const PopoverContent = React.forwardRef<
+  React.ElementRef<typeof PopoverPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
+>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
+  <PopoverPrimitive.Portal>
+    <PopoverPrimitive.Content
+      ref={ref}
+      align={align}
+      sideOffset={sideOffset}
+      className={cn(
+        'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 origin-[--radix-popover-content-transform-origin]',
+        className,
+      )}
+      {...props}
+    />
+  </PopoverPrimitive.Portal>
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+
+export { Popover, PopoverTrigger, PopoverContent };
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5ea779d402..537ef93b47 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -52,7 +52,7 @@ importers:
         version: 2.4.3(@solana/web3.js@1.98.0(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10))(@types/react@19.0.11)(bs58@5.0.0)(bufferutil@4.0.9)(immer@10.1.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2)(uploadthing@7.5.2(express@4.21.2)(h3@1.15.0)(next@15.2.1(@babel/core@7.26.9)(@playwright/test@1.50.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.0))(tailwindcss@3.4.3(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))))(use-sync-external-store@1.4.0(react@19.0.0))(utf-8-validate@5.0.10)(zod@3.24.2)
       '@privy-io/wagmi':
         specifier: ^1.0.3
-        version: 1.0.3(o5n3qiu3b4tooswfb6glcmvp3e)
+        version: 1.0.3(a1faf806091b0efe15f76ec15a7efe3e)
       '@radix-ui/react-aspect-ratio':
         specifier: ^1.1.2
         version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -60,7 +60,7 @@ importers:
         specifier: ^1.1.2
         version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@radix-ui/react-dialog':
-        specifier: ^1.1.4
+        specifier: ^1.1.6
         version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@radix-ui/react-dropdown-menu':
         specifier: ^2.1.4
@@ -71,6 +71,9 @@ importers:
       '@radix-ui/react-label':
         specifier: ^2.1.2
         version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-popover':
+        specifier: ^1.1.7
+        version: 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@radix-ui/react-progress':
         specifier: ^1.1.2
         version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -158,6 +161,9 @@ importers:
       clsx:
         specifier: ^2.1.1
         version: 2.1.1
+      cmdk:
+        specifier: ^1.1.1
+        version: 1.1.1(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       commander:
         specifier: ^13.1.0
         version: 13.1.0
@@ -320,13 +326,13 @@ importers:
         version: 0.15.10(@nomicfoundation/hardhat-verify@2.0.12(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)
       '@nomicfoundation/hardhat-ignition-ethers':
         specifier: ^0.15.0
-        version: 0.15.9(xgojni5muvd4kzkx4hpkhdjmq4)
+        version: 0.15.9(badd3d95e0cf79b498a019f1b6778749)
       '@nomicfoundation/hardhat-network-helpers':
         specifier: ^1.0.0
         version: 1.0.12(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))
       '@nomicfoundation/hardhat-toolbox':
         specifier: ^5.0.0
-        version: 5.0.0(6cfnoh2aclniwcobl6to5zqenm)
+        version: 5.0.0(ed72499a6fea5ec3bbcb412983a82c07)
       '@nomicfoundation/hardhat-verify':
         specifier: ^2.0.0
         version: 2.0.12(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))
@@ -353,7 +359,7 @@ importers:
         version: 20.4.4(@babel/traverse@7.26.9)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@swc/types@0.1.19)(typescript@5.7.2))(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(nx@20.4.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@swc/types@0.1.19)(typescript@5.7.2))(@swc/core@1.5.29(@swc/helpers@0.5.15)))(typescript@5.7.2)
       '@nx/next':
         specifier: 20.4.4
-        version: 20.4.4(lzhynl3p6ekz6ie5infmzcsbzu)
+        version: 20.4.4(9c3d00df8b4e0eb92a10dab5926a67e9)
       '@nx/node':
         specifier: 20.4.4
         version: 20.4.4(@babel/traverse@7.26.9)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@swc/types@0.1.19)(typescript@5.7.2))(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.22.0(jiti@2.4.2))(nx@20.4.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@swc/types@0.1.19)(typescript@5.7.2))(@swc/core@1.5.29(@swc/helpers@0.5.15)))(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)
@@ -4192,6 +4198,9 @@ packages:
   '@radix-ui/primitive@1.1.1':
     resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
 
+  '@radix-ui/primitive@1.1.2':
+    resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
+
   '@radix-ui/react-accessible-icon@1.1.2':
     resolution: {integrity: sha512-+rnMO0SEfzkcHr93RshkQVpOA26MtGOv4pcS9QUnLg4F8+GDmCJ8c2FEPhPz5e7arf31EzbTqJxFbzg3qen14g==}
     peerDependencies:
@@ -4244,6 +4253,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-arrow@1.1.3':
+    resolution: {integrity: sha512-2dvVU4jva0qkNZH6HHWuSz5FN5GeU5tymvCgutF8WaXz9WnD1NgUhy73cqzkjkN4Zkn8lfTPv5JIfrC221W+Nw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-aspect-ratio@1.1.2':
     resolution: {integrity: sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==}
     peerDependencies:
@@ -4318,6 +4340,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-compose-refs@1.1.2':
+    resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-context-menu@2.2.6':
     resolution: {integrity: sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==}
     peerDependencies:
@@ -4340,6 +4371,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-context@1.1.2':
+    resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-dialog@1.1.6':
     resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
     peerDependencies:
@@ -4375,6 +4415,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-dismissable-layer@1.1.6':
+    resolution: {integrity: sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-dropdown-menu@2.1.6':
     resolution: {integrity: sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==}
     peerDependencies:
@@ -4397,6 +4450,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-focus-guards@1.1.2':
+    resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-focus-scope@1.1.2':
     resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==}
     peerDependencies:
@@ -4410,6 +4472,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-focus-scope@1.1.3':
+    resolution: {integrity: sha512-4XaDlq0bPt7oJwR+0k0clCiCO/7lO7NKZTAaJBYxDNQT/vj4ig0/UvctrRscZaFREpRvUTkpKR96ov1e6jptQg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-form@0.1.2':
     resolution: {integrity: sha512-Owj1MjLq6/Rp85bgzYI+zRK5APLiWDtXDM63Z39FW15bNdehrcS+FjQgLGQYswFzipYu4GAA+t5w/VqvvNZ3ag==}
     peerDependencies:
@@ -4450,6 +4525,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-id@1.1.1':
+    resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-label@2.1.2':
     resolution: {integrity: sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==}
     peerDependencies:
@@ -4515,6 +4599,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-popover@1.1.7':
+    resolution: {integrity: sha512-I38OYWDmJF2kbO74LX8UsFydSHWOJuQ7LxPnTefjxxvdvPLempvAnmsyX9UsBlywcbSGpRH7oMLfkUf+ij4nrw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-popper@1.2.2':
     resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==}
     peerDependencies:
@@ -4528,6 +4625,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-popper@1.2.3':
+    resolution: {integrity: sha512-iNb9LYUMkne9zIahukgQmHlSBp9XWGeQQ7FvUGNk45ywzOb6kQa+Ca38OphXlWDiKvyneo9S+KSJsLfLt8812A==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-portal@1.1.4':
     resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==}
     peerDependencies:
@@ -4541,6 +4651,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-portal@1.1.5':
+    resolution: {integrity: sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-presence@1.1.2':
     resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==}
     peerDependencies:
@@ -4554,6 +4677,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-presence@1.1.3':
+    resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-primitive@2.0.2':
     resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==}
     peerDependencies:
@@ -4567,6 +4703,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-primitive@2.0.3':
+    resolution: {integrity: sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-progress@1.1.2':
     resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==}
     peerDependencies:
@@ -4667,6 +4816,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-slot@1.2.0':
+    resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-switch@1.1.3':
     resolution: {integrity: sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==}
     peerDependencies:
@@ -4767,6 +4925,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-callback-ref@1.1.1':
+    resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-controllable-state@1.1.0':
     resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
     peerDependencies:
@@ -4776,6 +4943,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-controllable-state@1.1.1':
+    resolution: {integrity: sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-escape-keydown@1.1.0':
     resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
     peerDependencies:
@@ -4785,6 +4961,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-escape-keydown@1.1.1':
+    resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-layout-effect@1.1.0':
     resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
     peerDependencies:
@@ -4794,6 +4979,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-layout-effect@1.1.1':
+    resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-previous@1.1.0':
     resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
     peerDependencies:
@@ -4812,6 +5006,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-rect@1.1.1':
+    resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-size@1.1.0':
     resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
     peerDependencies:
@@ -4821,6 +5024,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-size@1.1.1':
+    resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-visually-hidden@1.1.2':
     resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==}
     peerDependencies:
@@ -4837,6 +5049,9 @@ packages:
   '@radix-ui/rect@1.1.0':
     resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
 
+  '@radix-ui/rect@1.1.1':
+    resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
   '@radix-ui/themes@3.2.1':
     resolution: {integrity: sha512-WJL2YKAGItkunwm3O4cLTFKCGJTfAfF6Hmq7f5bCo1ggqC9qJQ/wfg/25AAN72aoEM1yqXZQ+pslsw48AFR0Xg==}
     peerDependencies:
@@ -8551,6 +8766,12 @@ packages:
     resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
     engines: {node: '>=6'}
 
+  cmdk@1.1.1:
+    resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
+    peerDependencies:
+      react: ^18 || ^19 || ^19.0.0-rc
+      react-dom: ^18 || ^19 || ^19.0.0-rc
+
   co@4.6.0:
     resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
     engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -21044,7 +21265,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@nomicfoundation/hardhat-ignition-ethers@0.15.9(xgojni5muvd4kzkx4hpkhdjmq4)':
+  '@nomicfoundation/hardhat-ignition-ethers@0.15.9(badd3d95e0cf79b498a019f1b6778749)':
     dependencies:
       '@nomicfoundation/hardhat-ethers': 3.0.8(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))
       '@nomicfoundation/hardhat-ignition': 0.15.10(@nomicfoundation/hardhat-verify@2.0.12(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)
@@ -21073,11 +21294,11 @@ snapshots:
       ethereumjs-util: 7.1.5
       hardhat: 2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10)
 
-  '@nomicfoundation/hardhat-toolbox@5.0.0(6cfnoh2aclniwcobl6to5zqenm)':
+  '@nomicfoundation/hardhat-toolbox@5.0.0(ed72499a6fea5ec3bbcb412983a82c07)':
     dependencies:
       '@nomicfoundation/hardhat-chai-matchers': 2.0.8(@nomicfoundation/hardhat-ethers@3.0.8(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10)))(chai@4.5.0)(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))
       '@nomicfoundation/hardhat-ethers': 3.0.8(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))
-      '@nomicfoundation/hardhat-ignition-ethers': 0.15.9(xgojni5muvd4kzkx4hpkhdjmq4)
+      '@nomicfoundation/hardhat-ignition-ethers': 0.15.9(badd3d95e0cf79b498a019f1b6778749)
       '@nomicfoundation/hardhat-network-helpers': 1.0.12(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))
       '@nomicfoundation/hardhat-verify': 2.0.12(hardhat@2.22.18(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))(typescript@5.7.2)(utf-8-validate@5.0.10))
       '@typechain/ethers-v6': 0.5.1(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typechain@8.3.2(typescript@5.7.2))(typescript@5.7.2)
@@ -21573,7 +21794,7 @@ snapshots:
       - vue-tsc
       - webpack-cli
 
-  '@nx/next@20.4.4(lzhynl3p6ekz6ie5infmzcsbzu)':
+  '@nx/next@20.4.4(9c3d00df8b4e0eb92a10dab5926a67e9)':
     dependencies:
       '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.9)
       '@nx/devkit': 20.4.4(nx@20.4.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@swc/types@0.1.19)(typescript@5.7.2))(@swc/core@1.5.29(@swc/helpers@0.5.15)))
@@ -22430,7 +22651,7 @@ snapshots:
       - utf-8-validate
       - zod
 
-  '@privy-io/wagmi@1.0.3(o5n3qiu3b4tooswfb6glcmvp3e)':
+  '@privy-io/wagmi@1.0.3(a1faf806091b0efe15f76ec15a7efe3e)':
     dependencies:
       '@privy-io/react-auth': 2.4.3(@solana/web3.js@1.98.0(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10))(@types/react@19.0.11)(bs58@5.0.0)(bufferutil@4.0.9)(immer@10.1.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2)(uploadthing@7.5.2(express@4.21.2)(h3@1.15.0)(next@15.2.1(@babel/core@7.26.9)(@playwright/test@1.50.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.0))(tailwindcss@3.4.3(ts-node@10.9.2(@swc/core@1.5.29(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.7.2))))(use-sync-external-store@1.4.0(react@19.0.0))(utf-8-validate@5.0.10)(zod@3.24.2)
       react: 19.0.0
@@ -22443,6 +22664,8 @@ snapshots:
 
   '@radix-ui/primitive@1.1.1': {}
 
+  '@radix-ui/primitive@1.1.2': {}
+
   '@radix-ui/react-accessible-icon@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -22492,6 +22715,15 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-arrow@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-aspect-ratio@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -22563,6 +22795,12 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-compose-refs@1.1.2(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-context-menu@2.2.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/primitive': 1.1.1
@@ -22583,6 +22821,12 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-context@1.1.2(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/primitive': 1.1.1
@@ -22624,6 +22868,19 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-dismissable-layer@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-dropdown-menu@2.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/primitive': 1.1.1
@@ -22645,6 +22902,12 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-focus-guards@1.1.2(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
@@ -22656,6 +22919,17 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-focus-scope@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-form@0.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/primitive': 1.1.1
@@ -22698,6 +22972,13 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-id@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-label@2.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -22796,6 +23077,29 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-popover@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-dismissable-layer': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-focus-scope': 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-id': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-popper': 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-portal': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.2.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      aria-hidden: 1.2.4
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+      react-remove-scroll: 2.6.3(@types/react@19.0.11)(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-popper@1.2.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -22814,6 +23118,24 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-popper@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-arrow': 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-rect': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-size': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/rect': 1.1.1
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-portal@1.1.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -22824,6 +23146,16 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-portal@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
@@ -22834,6 +23166,16 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-presence@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
@@ -22843,6 +23185,15 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-primitive@2.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-slot': 1.2.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-progress@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
@@ -22969,6 +23320,13 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-slot@1.2.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-switch@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/primitive': 1.1.1
@@ -23087,6 +23445,12 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
@@ -23094,6 +23458,13 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-controllable-state@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
@@ -23101,12 +23472,25 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       react: 19.0.0
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-previous@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       react: 19.0.0
@@ -23120,6 +23504,13 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-rect@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/rect': 1.1.1
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-size@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
@@ -23127,6 +23518,13 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-size@1.1.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -23138,6 +23536,8 @@ snapshots:
 
   '@radix-ui/rect@1.1.0': {}
 
+  '@radix-ui/rect@1.1.1': {}
+
   '@radix-ui/themes@3.2.1(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/colors': 3.0.0
@@ -28328,6 +28728,18 @@ snapshots:
 
   clsx@2.1.1: {}
 
+  cmdk@1.1.1(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-dialog': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-id': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    transitivePeerDependencies:
+      - '@types/react'
+      - '@types/react-dom'
+
   co@4.6.0: {}
 
   code-block-writer@12.0.0: {}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>packages/epics/src/spaces/components/create-space-form.tsx (1)</summary><blockquote>

`165-196`: **Consider getting category options from a central source.**

The implementation of the categories form field looks good, but the category options are hardcoded in the component. For better maintainability, consider importing these values from the same central source as the database schema.


```diff
<MultiSelect
  placeholder={'Select Categories'}
- options={[
-   { value: 'housing', label: 'Housing' },
-   { value: 'energy', label: 'Energy' },
-   { value: 'mobility', label: 'Mobility' },
-   { value: 'water', label: 'Water' },
-   { value: 'air', label: 'Air' },
-   { value: 'soil', label: 'Soil' },
-   { value: 'flora', label: 'Flora' },
-   { value: 'fauna', label: 'Fauna' },
-   { value: 'fungi', label: 'Fungi' },
-   { value: 'food', label: 'Food' },
-   { value: 'education', label: 'Education' },
-   { value: 'art', label: 'Art' },
-   { value: 'health', label: 'Health' },
-   { value: 'tech', label: 'Tech' },
- ]}
+ options={CATEGORY_OPTIONS}
  defaultValue={field.value}
  onValueChange={field.onChange}
/>
```

Where `CATEGORY_OPTIONS` could be imported from a shared constants file that derives from the same source as the database schema, ensuring consistency across the application.

</blockquote></details>
<details>
<summary>packages/ui/src/chips.tsx (1)</summary><blockquote>

`222-237`: **Possible typo in opacity value**

The opacity value in `border-foreground/1` seems unusually low. Tailwind opacity values typically range from 0-100, with common increments of 5, 10, etc.

Consider using a more standard opacity value:

```diff
-'bg-transparent text-foreground border-foreground/1 hover:bg-transparent',
+'bg-transparent text-foreground border-foreground/10 hover:bg-transparent',
```

</blockquote></details>
<details>
<summary>packages/ui/src/command.tsx (1)</summary><blockquote>

`117-119`: **Inconsistent size utility usage**

The component uses `size-4` for SVG sizing, which is a newer Tailwind utility, but uses `h-4 w-4` elsewhere in the codebase. For consistency, it's better to standardize on one approach.

Consider standardizing the sizing approach:

```diff
-"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0",
```

</blockquote></details>
<details>
<summary>packages/storage-postgres/migrations/meta/0020_snapshot.json (1)</summary><blockquote>

`7-104`: **Documents table schema needs referential integrity improvement**

The documents table is well-structured with appropriate fields and constraints. However, the foreign key constraints use "no action" for both onDelete and onUpdate, which could lead to orphaned records if a referenced space or creator is deleted.


Consider modifying the foreign key constraints to use "CASCADE" or "SET NULL" for onDelete, depending on your business requirements:

```diff
"onDelete": "no action",
+"onDelete": "CASCADE",
```

or

```diff
"onDelete": "no action",
+"onDelete": "SET NULL",
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 045cc9e04af790481322ad870bb687a5d2555bce and 90f1b4cad966f436f68c78b99fbc752872c4a0fc.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `package.json` (2 hunks)
* `packages/core/src/space/validation.ts` (1 hunks)
* `packages/epics/src/spaces/components/create-space-form.tsx` (3 hunks)
* `packages/storage-postgres/migrations/0020_wide_johnny_blaze.sql` (1 hunks)
* `packages/storage-postgres/migrations/meta/0020_snapshot.json` (1 hunks)
* `packages/storage-postgres/migrations/meta/_journal.json` (1 hunks)
* `packages/storage-postgres/src/schema/categories.ts` (1 hunks)
* `packages/storage-postgres/src/schema/space.ts` (2 hunks)
* `packages/ui/src/chips.stories.tsx` (1 hunks)
* `packages/ui/src/chips.tsx` (1 hunks)
* `packages/ui/src/command.tsx` (1 hunks)
* `packages/ui/src/dialog.tsx` (1 hunks)
* `packages/ui/src/index.ts` (1 hunks)
* `packages/ui/src/popover.tsx` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (5)</summary>

<details>
<summary>packages/storage-postgres/src/schema/space.ts (1)</summary><blockquote>

<details>
<summary>packages/storage-postgres/src/schema/categories.ts (1)</summary>

* `categories` (3-18)

</details>

</blockquote></details>
<details>
<summary>packages/epics/src/spaces/components/create-space-form.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/chips.tsx (1)</summary>

* `MultiSelect` (109-352)

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/chips.tsx (4)</summary><blockquote>

<details>
<summary>packages/ui/src/popover.tsx (3)</summary>

* `Popover` (31-31)
* `PopoverTrigger` (31-31)
* `PopoverContent` (31-31)

</details>
<details>
<summary>packages/ui/src/button.tsx (1)</summary>

* `Button` (129-129)

</details>
<details>
<summary>packages/ui/src/separator.tsx (1)</summary>

* `Separator` (29-29)

</details>
<details>
<summary>packages/ui/src/command.tsx (7)</summary>

* `Command` (144-144)
* `CommandInput` (146-146)
* `CommandList` (147-147)
* `CommandEmpty` (148-148)
* `CommandGroup` (149-149)
* `CommandItem` (150-150)
* `CommandSeparator` (152-152)

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/chips.stories.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/chips.tsx (1)</summary>

* `MultiSelect` (109-352)

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/command.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/dialog.tsx (2)</summary>

* `Dialog` (112-112)
* `DialogContent` (117-117)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: deploy-preview
* GitHub Check: main

</details>

<details>
<summary>🔇 Additional comments (27)</summary><blockquote>

<details>
<summary>packages/storage-postgres/migrations/0020_wide_johnny_blaze.sql (1)</summary>

`1-1`: **LGTM! Schema update for categories looks good.**

The migration properly adds a `categories` column to the `spaces` table with a jsonb type and a default empty array, which is appropriate for storing multiple categories per space.

</details>
<details>
<summary>packages/ui/src/index.ts (1)</summary>

`10-10`: **LGTM! New chips module export added correctly.**

The export for the chips module (which contains the MultiSelect component) has been correctly added to the UI package's main export file.

</details>
<details>
<summary>packages/storage-postgres/migrations/meta/_journal.json (1)</summary>

`144-151`: **LGTM! Migration journal updated correctly.**

The migration journal entry has been properly added with a sequential index (20), consistent version (7), appropriate timestamp, and correct reference to the migration file.

</details>
<details>
<summary>packages/core/src/space/validation.ts (1)</summary>

`20-20`: **New categories field looks good.**

The addition of the categories field with the appropriate Zod validation and default empty array aligns well with the feature requirements.

</details>
<details>
<summary>package.json (1)</summary>

`35-35`: **Package dependencies additions look good.**

The updates to `@radix-ui/react-dialog` and the additions of `@radix-ui/react-popover` and `cmdk` are appropriate for supporting the new category selector UI components.



Also applies to: 39-39, 69-69

</details>
<details>
<summary>packages/epics/src/spaces/components/create-space-form.tsx (1)</summary>

`17-17`: **MultiSelect import and categories default value look good.**

The `MultiSelect` component import and default empty array for categories are correctly implemented.



Also applies to: 64-64

</details>
<details>
<summary>packages/ui/src/popover.tsx (1)</summary>

`1-31`: **Well-structured Popover component with good use of Radix UI primitives**

This implementation creates a clean, reusable Popover component leveraging Radix UI primitives. The component is properly typed with TypeScript, includes good default values for common properties (align and sideOffset), and uses portals for proper DOM rendering.

The styling is comprehensive with nice animations based on the popover's state (open/closed) and appearance side (top/bottom/left/right). The use of `forwardRef` allows proper DOM access which is essential for accessibility and imperative control when needed.

</details>
<details>
<summary>packages/ui/src/chips.stories.tsx (3)</summary>

`8-23`: **Good selection of category options with clear labels**

The frameworks list provides a comprehensive set of category options that cover various domains (housing, energy, mobility, etc.), which aligns well with the categorization feature being implemented.

---

`25-30`: **Storybook configuration looks good**

The metadata configuration for the Storybook story is properly set up with the correct component reference and appropriate title.

---

`32-41`: **Well-defined primary story with sensible defaults**

The Primary story is configured with appropriate default values, including empty selection, a clear placeholder, sensible animation duration, and reasonable maxCount limit.

</details>
<details>
<summary>packages/ui/src/chips.tsx (4)</summary>

`23-41`: **Well-defined style variants with meaningful options**

The multiSelectVariants implementation provides a good set of styling options with clear variant names. The transition effects add a nice touch to the user experience.

---

`46-107`: **Excellent prop documentation with comprehensive JSDoc comments**

The interface definition is thorough with detailed JSDoc comments for each prop, making it clear how to use the component. The props cover a wide range of customization options which enhances the component's reusability.

---

`109-177`: **Clean state management and event handlers**

The component manages its state well with clear, focused event handlers for various user interactions. The functions for toggling options, clearing selections, and handling keyboard events are well-implemented.

---

`179-349`: **Rich UI implementation with good accessibility considerations**

The UI implementation provides a comprehensive selection experience with features like:
- Visual indication of selected items
- "Select all" functionality
- Search capabilities
- Keyboard navigation support
- Clear option for selected values

The component leverages the Command and Popover components effectively to create a cohesive selection interface.

</details>
<details>
<summary>packages/ui/src/command.tsx (3)</summary>

`1-36`: **Well-structured Command components with good integration of cmdk**

The Command and CommandDialog components are well-implemented, with proper TypeScript typing and forwarded refs. The styling is comprehensive and leverages CSS selectors smartly to target nested cmdk elements.

---

`38-81`: **Good implementation of input and list components**

The CommandInput component nicely integrates a search icon and appropriate styling. The CommandList and CommandEmpty components handle the display of options and the empty state with good defaults for max height and scrolling behavior.

---

`83-140`: **Well-designed command item components**

The CommandGroup, CommandSeparator, CommandItem, and CommandShortcut components follow consistent patterns and have appropriate styling for their respective roles. The data attributes for states like selected and disabled are properly utilized.

</details>
<details>
<summary>packages/ui/src/dialog.tsx (6)</summary>

`1-16`: **Well-structured imports and component setup**

The file is properly set up with the 'use client' directive, necessary imports, and basic component definitions derived from Radix UI primitives. This approach ensures good accessibility out of the box.

---

`17-30`: **Good implementation of the DialogOverlay component**

The DialogOverlay component correctly uses React.forwardRef to maintain the component reference chain. The styling uses data attributes for state-based animations which is the recommended approach with Radix UI components.

---

`32-54`: **DialogContent implementation follows accessibility best practices**

The implementation includes:
- Proper positioning and responsive design
- State-based animations for smooth transitions
- An accessible close button with both visual (X icon) and screen reader support
- Correctly forwarded refs and props

The close button implementation with the hidden "Close" text for screen readers is particularly good for accessibility.

---

`56-82`: **Well-designed header and footer components**

Both DialogHeader and DialogFooter are implemented with responsive design in mind:
- DialogHeader maintains proper spacing and text alignment
- DialogFooter uses a responsive layout that adapts to different screen sizes, switching from column to row layout on larger screens

This improves usability across different devices.

---

`84-109`: **Consistent implementation of title and description components**

DialogTitle and DialogDescription are properly implemented with:
- Correct forwarding of refs
- Appropriate typography styling
- Proper display names for better debugging experience

These components follow the same pattern as the other dialog components, maintaining consistency.

---

`111-122`: **Complete export of all dialog components**

All dialog components are properly exported, allowing for flexible composition when using this module. This granular export approach is preferred for component libraries as it enables tree-shaking.

</details>
<details>
<summary>packages/storage-postgres/migrations/meta/0020_snapshot.json (4)</summary>

`1-5`: **Migration metadata is properly defined**

The migration is correctly set up with a unique ID, reference to the previous migration, version number, and dialect specification.

---

`105-198`: **Memberships table has appropriate unique constraint**

The memberships table correctly enforces that a person can only be a member of a space once through the unique index on (person_id, space_id). The btree index method is also appropriate for this use case.

---

`399-412`: **New categories field added to spaces table**

The new categories field is appropriately implemented as a JSONB type with a default empty array value. This provides flexibility for storing multiple categories per space and aligns with the categorization feature being implemented.

---

`446-454`: **Document state enum is well-defined**

The document_state enum includes appropriate values (discussion, proposal, agreement) that logically represent the progression of a document's lifecycle.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:59:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>packages/core/src/space/types.ts (2)</summary><blockquote>

`11-11`: **Consider type consistency between `Space` and `CreateSpaceInput`**

The `categories` field in `Space` allows any string array, while `CreateSpaceInput` restricts to specific string literals. This inconsistency might cause type safety issues if arbitrary strings are stored in a `Space` object but the UI only expects predefined categories.

Consider using the same union type for both interfaces:

```diff
-  categories?: string[] | null;
+  categories?: (
+    | 'housing'
+    | 'energy'
+    | 'mobility'
+    | 'water'
+    | 'air'
+    | 'soil'
+    | 'flora'
+    | 'fauna'
+    | 'fungi'
+    | 'food'
+    | 'education'
+    | 'art'
+    | 'health'
+    | 'tech'
+  )[] | null;
```

---

`22-37`: **Consider extracting category literals to a shared enum or type**

The category literals are hardcoded in the `CreateSpaceInput` interface. If these categories need to be referenced elsewhere, having them defined in multiple places could lead to inconsistencies.

Extract the category literals to a shared type or enum:

```typescript
export type SpaceCategory =
  | 'housing'
  | 'energy'
  | 'mobility'
  | 'water'
  | 'air'
  | 'soil'
  | 'flora'
  | 'fauna'
  | 'fungi'
  | 'food'
  | 'education'
  | 'art'
  | 'health'
  | 'tech';

// Then use this type in the interfaces
export interface CreateSpaceInput {
  // ...other properties
  categories?: SpaceCategory[];
}
```

</blockquote></details>
<details>
<summary>apps/web/src/app/[lang]/network/page.tsx (2)</summary><blockquote>

`41-57`: **Add handling for empty `uniqueCategories` array**

If there are no spaces with categories, the current code will only show the "No Category" slider. Consider adding a check to handle this case gracefully.

```diff
- {uniqueCategories.map((category) => (
-   <SpaceGroupSlider
-     key={category}
-     spaces={spaces.filter(
-       (space) => space.categories && space.categories.includes(category),
-     )}
-     type={category}
-     getHref={getHref}
-   />
- ))}
+ {uniqueCategories.length > 0 ? (
+   uniqueCategories.map((category) => (
+     <SpaceGroupSlider
+       key={category}
+       spaces={spaces.filter(
+         (space) => space.categories?.includes(category),
+       )}
+       type={category}
+       getHref={getHref}
+     />
+   ))
+ ) : null}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 45-45: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`55-55`: **Consider using i18n for "No Category" label**

The literal string 'No Category' is hardcoded. If the application supports internationalization (i18n), this string should be localized.

```diff
- type={'No Category'}
+ type={t('spaces.noCategory')}
```

You would need to add the translation key to your i18n files.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 90f1b4cad966f436f68c78b99fbc752872c4a0fc and d494f8e113dea5a9266a32880f272d9daf5bf3a0.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `apps/web/src/app/[lang]/network/page.tsx` (2 hunks)
* `packages/core/src/space/types.ts` (2 hunks)
* `packages/core/src/space/validation.ts` (1 hunks)
* `packages/epics/src/spaces/components/space-group-slider.tsx` (1 hunks)
* `packages/storage-postgres/src/schema/categories.ts` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* packages/epics/src/spaces/components/space-group-slider.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* packages/core/src/space/validation.ts
* packages/storage-postgres/src/schema/categories.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/web/src/app/[lang]/network/page.tsx</summary>

[error] 45-45: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: main

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:17:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/ui/src/chips.stories.tsx (3)</summary><blockquote>

`8-23`: **Consider enhancing options for better demonstration**

The `frameworksList` provides a good set of category options. To showcase more features of the MultiSelect component, consider adding the `icon` property to some of the options, as the component supports displaying icons alongside labels.


```diff
const frameworksList = [
-  { value: 'housing', label: 'Housing' },
+  { value: 'housing', label: 'Housing', icon: HomeIcon },
-  { value: 'energy', label: 'Energy' },
+  { value: 'energy', label: 'Energy', icon: ZapIcon },
  // Add icons to other options as appropriate
];
```

Don't forget to import the necessary icons at the top of the file:
```typescript
import { Home as HomeIcon, Zap as ZapIcon } from 'lucide-react';
```

---

`32-45`: **Consider adding more stories to showcase component variations**

The current implementation only includes a 'Primary' story. To improve component documentation and testing, consider adding additional stories that showcase different variants, states, or use cases of the MultiSelect component.


```typescript
export const WithDefaultValues: Story = {
  args: {
    ...Primary.args,
    defaultValue: ['housing', 'energy'],
  },
};

export const WithMaximumSelected: Story = {
  args: {
    ...Primary.args,
    defaultValue: ['housing', 'energy', 'mobility', 'water'],
  },
};

export const WithModalPopover: Story = {
  args: {
    ...Primary.args,
    modalPopover: true,
  },
};
```

---

`41-44`: **Enhance the play function with interaction tests**

The current play function only checks for the presence of the placeholder text. Consider expanding it to test interactions with the component, such as clicking the dropdown, selecting options, and verifying the selected values.


```diff
play: async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  expect(canvas.getByText(/Select Categories/gi)).toBeTruthy();
+  
+  // Click the dropdown to open it
+  const dropdown = canvas.getByText(/Select Categories/gi);
+  await userEvent.click(dropdown);
+  
+  // Select an option
+  const housingOption = canvas.getByText('Housing');
+  await userEvent.click(housingOption);
+  
+  // Verify the option was selected
+  expect(canvas.getByText('Housing')).toBeTruthy();
+  
+  // Close the dropdown
+  const closeButton = canvas.getByText('Close');
+  await userEvent.click(closeButton);
},
```

Don't forget to import userEvent at the top of the file:
```typescript
import userEvent from '@storybook/testing-library/user-event';
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d494f8e113dea5a9266a32880f272d9daf5bf3a0 and 2e19e4f8cbfef8f6a9089bc8f014df1fecd4f8df.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/ui/src/chips.stories.tsx` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/ui/src/chips.stories.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/chips.tsx (1)</summary>

* `MultiSelect` (109-352)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: deploy-preview
* GitHub Check: main

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>packages/ui/src/chips.stories.tsx (1)</summary>

`32-45`: **LGTM! Previous issue has been fixed**

The story has correctly implemented the test to check for the placeholder text "Select Categories" instead of the previously incorrect "Welcome to MultiSelect!" text. This aligns with the placeholder prop provided in the story args.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:23:03Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/storage-postgres/src/schema/categories.ts (1)</summary><blockquote>

`20-20`: **Enum name now correctly matches its content**

The Postgres enum is appropriately named 'categories' which properly reflects its purpose - an improvement over the previous naming that was identified in past reviews.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/storage-postgres/src/schema/categories.ts (1)</summary><blockquote>

`1-21`: **Consider adding documentation comments**

Adding JSDoc comments explaining the purpose of these categories and how they should be used would improve maintainability and help other developers understand this schema component's role in the application.


```diff
 import { pgEnum } from 'drizzle-orm/pg-core';

+/**
+ * List of available categories for spaces
+ * Used for categorization and filtering
+ */
 export const CATEGORIES = [
   'housing',
   'energy',
   'mobility',
   'water',
   'air',
   'soil',
   'flora',
   'fauna',
   'fungi',
   'food',
   'education',
   'art',
   'health',
   'tech',
 ] as const;

+/**
+ * Postgres enum for space categories
+ */
 export const categories = pgEnum('categories', CATEGORIES);
```

</blockquote></details>
<details>
<summary>packages/storage-postgres/src/seed.ts (2)</summary><blockquote>

`68-69`: **Consider more realistic category seeding**

While seeding every space with all categories works functionally, it might not represent realistic usage patterns. Consider randomly selecting a subset of categories for each space to better simulate real-world data.


```diff
-          links: f.valuesFromArray({ values: [] }),
-          categories: f.valuesFromArray({ values: [...CATEGORIES] }),
+          links: f.valuesFromArray({ values: [] }),
+          categories: f.valuesFromFunction({
+            callback: () => {
+              // Select a random number of categories (1-5)
+              const numCategories = Math.floor(Math.random() * 5) + 1;
+              // Shuffle and take a subset
+              return [...CATEGORIES]
+                .sort(() => Math.random() - 0.5)
+                .slice(0, numCategories);
+            }
+          }),
```

---

`68-68`: **The links property should be documented**

The purpose of the `links` property isn't immediately clear from the code. Consider adding a comment explaining what this property represents and how it will be used.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2e19e4f8cbfef8f6a9089bc8f014df1fecd4f8df and c4b4da9d1bf88f3b5104033a1de4f56b17ce6b84.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/storage-postgres/src/schema/categories.ts` (1 hunks)
* `packages/storage-postgres/src/seed.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/storage-postgres/src/seed.ts (1)</summary><blockquote>

<details>
<summary>packages/storage-postgres/src/schema/categories.ts (1)</summary>

* `CATEGORIES` (3-18)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: main

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>packages/storage-postgres/src/schema/categories.ts (1)</summary>

`3-18`: **Excellent choice of categories for environmental and social topics**

The comprehensive list covers key sustainability areas that align well with the project's focus. Using `as const` properly ensures type safety by making the array immutable and preserving the exact string literal types.

</details>
<details>
<summary>packages/storage-postgres/src/seed.ts (1)</summary>

`7-7`: **Appropriate imports for the new categories schema**

The import statement correctly includes both the `CATEGORIES` constant and the `categories` enum from the schema definition file.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T16:12:39Z
user: login: evgenibir
body: looks good
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-11T06:16:13Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/epics/src/spaces/components/create-space-form.tsx (1)</summary><blockquote>

`165-197`: **Consider label consistency and extracting category options.**

The field name is "categories" but the label shown to users is "Tags". This inconsistency might be confusing for developers maintaining the code. Additionally, the hardcoded category options could be extracted to a constants file for better maintainability.


Consider these improvements:

```diff
+ // Import at the top of the file
+ import { SPACE_CATEGORIES } from '@hypha-platform/core/client';

  <FormField
    control={form.control}
    name="categories"
    render={({ field }) => (
      <FormItem>
-       <FormLabel>Tags</FormLabel>
+       <FormLabel>Categories</FormLabel>
        <FormControl>
          <MultiSelect
            placeholder={'Select one or more'}
-           options={[
-             { value: 'housing', label: 'Housing' },
-             { value: 'energy', label: 'Energy' },
-             { value: 'mobility', label: 'Mobility' },
-             { value: 'water', label: 'Water' },
-             { value: 'air', label: 'Air' },
-             { value: 'soil', label: 'Soil' },
-             { value: 'flora', label: 'Flora' },
-             { value: 'fauna', label: 'Fauna' },
-             { value: 'fungi', label: 'Fungi' },
-             { value: 'food', label: 'Food' },
-             { value: 'education', label: 'Education' },
-             { value: 'art', label: 'Art' },
-             { value: 'health', label: 'Health' },
-             { value: 'tech', label: 'Tech' },
-           ]}
+           options={SPACE_CATEGORIES}
            defaultValue={field.value}
            onValueChange={field.onChange}
          />
        </FormControl>
        <FormMessage />
      </FormItem>
    )}
  />
```

Then in a constants file:
```typescript
// In a constants file like packages/core/src/space/constants.ts
export const SPACE_CATEGORIES = [
  { value: 'housing', label: 'Housing' },
  { value: 'energy', label: 'Energy' },
  { value: 'mobility', label: 'Mobility' },
  { value: 'water', label: 'Water' },
  { value: 'air', label: 'Air' },
  { value: 'soil', label: 'Soil' },
  { value: 'flora', label: 'Flora' },
  { value: 'fauna', label: 'Fauna' },
  { value: 'fungi', label: 'Fungi' },
  { value: 'food', label: 'Food' },
  { value: 'education', label: 'Education' },
  { value: 'art', label: 'Art' },
  { value: 'health', label: 'Health' },
  { value: 'tech', label: 'Tech' },
];
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c4b4da9d1bf88f3b5104033a1de4f56b17ce6b84 and 8ea437f66c175c7b0486e8a993b19a3959b6f221.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/epics/src/spaces/components/create-space-form.tsx` (3 hunks)
* `packages/storage-postgres/src/seed.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/storage-postgres/src/seed.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/epics/src/spaces/components/create-space-form.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/chips.tsx (1)</summary>

* `MultiSelect` (109-352)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: main
* GitHub Check: deploy-preview

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>packages/epics/src/spaces/components/create-space-form.tsx (3)</summary>

`17-17`: **LGTM: Good addition of MultiSelect component import.**

The MultiSelect component is properly imported from the UI library along with other form components.

---

`64-64`: **LGTM: Proper initialization of categories field.**

The categories field is correctly initialized as an empty array in the default values, which is appropriate for a multi-select component.

---

`172-192`: **Verify MultiSelect component integration.**

The MultiSelect component looks properly integrated, but let's check if we need to consider:

1. Max selection limit - Is there a business requirement limiting how many categories a user can select?
2. Default sorting - Should the categories have a specific order or grouping?
3. Performance - With 14 options, this should be fine, but keep in mind if the list grows substantially.


Please confirm if there are any business requirements around maximum number of categories per space, or if any specific sorting/grouping is needed for the categories.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:31:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>packages/ui/src/chips.tsx (1)</summary><blockquote>

`1-6`: **Inconsistent utility import path**

The `cn` utility is imported from `@hypha-platform/ui-utils` here, which is consistent with other files like popover.tsx.

</blockquote></details>
<details>
<summary>apps/web/src/app/[lang]/network/page.tsx (2)</summary><blockquote>

`46-46`: **LGTM! Good use of optional chaining**

You've correctly implemented the optional chaining suggestion from the previous review.

---

`53-55`: _⚠️ Potential issue_

**Fix logical error in filter condition**

The current condition `space.categories && space.categories.length === 0` is a logical contradiction. If `space.categories` exists and is truthy, it's unlikely to have a length of 0. You probably meant to filter spaces that either don't have categories or have empty categories.



```diff
- (space) => space.categories && space.categories.length === 0,
+ (space) => !space.categories || space.categories.length === 0,
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/ui/src/chips.tsx (3)</summary><blockquote>

`19-41`: **Animation duration could benefit from more user control**

The transition defined in the multiSelectVariants uses hardcoded animation values (`delay-150`, `duration-300`) while allowing animation duration to be customized via props elsewhere.


Consider parameterizing all animation values to be controlled through props for consistency:

```diff
 const multiSelectVariants = cva(
-  'm-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300',
+  'm-1 transition ease-in-out hover:-translate-y-1 hover:scale-110',
   {
     variants: {
       variant: {
```

---

`133-144`: **Consider enhancing keyboard accessibility**

The component handles Enter and Backspace keys, but could be improved with additional keyboard navigation support.


Consider adding more keyboard support:
- Arrow keys for navigating options
- Space key for selecting/deselecting
- Escape key for closing popover (though this may be handled by the Popover component)

```diff
 const handleInputKeyDown = (
   event: React.KeyboardEvent<HTMLInputElement>,
 ) => {
   if (event.key === 'Enter') {
     setIsPopoverOpen(true);
   } else if (event.key === 'Backspace' && !event.currentTarget.value) {
     const newSelectedValues = [...selectedValues];
     newSelectedValues.pop();
     setSelectedValues(newSelectedValues);
     onValueChange(newSelectedValues);
+  } else if (event.key === 'Escape') {
+    setIsPopoverOpen(false);
   }
 };
```

---

`298-318`: **Add visual/focus indication when navigating with keyboard**

The CommandItem needs a visual indication when focused using keyboard navigation.


Consider adding focus state styles to improve keyboard navigation:

```diff
 <CommandItem
   key={option.value}
   onSelect={() => toggleOption(option.value)}
-  className="cursor-pointer"
+  className="cursor-pointer focus:bg-accent focus:text-accent-foreground"
 >
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8ea437f66c175c7b0486e8a993b19a3959b6f221 and 60926285b680f6ef0426ac0db845290325b55703.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `apps/web/src/app/[lang]/network/page.tsx` (2 hunks)
* `packages/core/src/categories/constants.ts` (1 hunks)
* `packages/core/src/categories/index.ts` (1 hunks)
* `packages/core/src/categories/types.ts` (1 hunks)
* `packages/core/src/client.ts` (1 hunks)
* `packages/core/src/space/types.ts` (3 hunks)
* `packages/epics/src/spaces/components/create-space-form.tsx` (4 hunks)
* `packages/ui/src/chips.tsx` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* packages/core/src/categories/index.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* packages/core/src/space/types.ts
* packages/epics/src/spaces/components/create-space-form.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>apps/web/src/app/[lang]/network/page.tsx (3)</summary><blockquote>

<details>
<summary>packages/core/src/space/types.ts (1)</summary>

* `Space` (3-14)

</details>
<details>
<summary>packages/core/src/categories/types.ts (1)</summary>

* `Category` (1-15)

</details>
<details>
<summary>packages/epics/src/spaces/components/space-group-slider.tsx (1)</summary>

* `SpaceGroupSlider` (20-62)

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/chips.tsx (3)</summary><blockquote>

<details>
<summary>packages/ui/src/popover.tsx (3)</summary>

* `Popover` (31-31)
* `PopoverTrigger` (31-31)
* `PopoverContent` (31-31)

</details>
<details>
<summary>packages/ui/src/button.tsx (1)</summary>

* `Button` (129-129)

</details>
<details>
<summary>packages/ui/src/command.tsx (7)</summary>

* `Command` (144-144)
* `CommandInput` (146-146)
* `CommandList` (147-147)
* `CommandEmpty` (148-148)
* `CommandGroup` (149-149)
* `CommandItem` (150-150)
* `CommandSeparator` (152-152)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/web/src/app/[lang]/network/page.tsx</summary>

[error] 16-16: Avoid the use of spread (`...`) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in `.reduce`) because it causes a time complexity of `O(n^2)`.
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: deploy-preview
* GitHub Check: main

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>packages/core/src/categories/constants.ts (1)</summary>

`1-16`: **Well-structured category definitions with clear labeling**

The implementation provides a clean array of category objects with consistent naming patterns (lowercase values, capitalized labels). The categories logically cover various domains relevant to spaces.

</details>
<details>
<summary>packages/ui/src/chips.tsx (1)</summary>

`43-107`: **Comprehensive props interface with excellent documentation**

The MultiSelectProps interface is well-documented with JSDoc comments explaining each prop's purpose, type, and default values. This is excellent practice for component libraries.

</details>
<details>
<summary>packages/core/src/categories/types.ts (1)</summary>

`1-15`: **Well-defined type that aligns with constants**

The Category type correctly defines all possible category values as a union type, matching exactly with the values defined in constants.ts.

</details>
<details>
<summary>packages/core/src/client.ts (1)</summary>

`1-1`: **Appropriate export pattern for categories module**

The added export statement follows the project's established pattern for exposing functionality, making the categories module accessible through the client.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:55:20Z

Review comments:
diff_hunk: @@ -0,0 +1,18 @@
+import { pgEnum } from 'drizzle-orm/pg-core';
+
+export const categories = pgEnum('document_state', [
+  'housing',
+  'energy',
+  'mobility',
+  'water',
+  'air',
+  'soil',
+  'flora',
+  'fauna',
+  'fungi',
+  'food',
+  'education',
+  'art',
+  'health',
+  'tech',
+]);
path: packages/storage-postgres/src/schema/categories.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Enum name doesn't match its content - consider renaming**

The enum is named `document_state` but contains category values like 'housing', 'energy', etc. This naming mismatch could cause confusion for other developers who might expect document states (draft, published) rather than categories.


Consider renaming the enum to better reflect its purpose:

```diff
-export const categories = pgEnum('document_state', [
+export const categories = pgEnum('category_type', [
  'housing',
  'energy',
  'mobility',
  'water',
  'air',
  'soil',
  'flora',
  'fauna',
  'fungi',
  'food',
  'education',
  'art',
  'health',
  'tech',
]);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export const categories = pgEnum('category_type', [
  'housing',
  'energy',
  'mobility',
  'water',
  'air',
  'soil',
  'flora',
  'fauna',
  'fungi',
  'food',
  'education',
  'art',
  'health',
  'tech',
]);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T12:59:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,7 @@
 import { InferInsertModel, InferSelectModel } from 'drizzle-orm';
 import { integer, jsonb, pgTable, serial, text } from 'drizzle-orm/pg-core';
 import { commonDateFields } from './shared';
+import { categories } from './categories';
path: packages/storage-postgres/src/schema/space.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Categories implementation has a naming inconsistency.**

The implementation correctly adds the categories field as a JSONB type with proper typing and default value. However, there's an inconsistency in the imported enum name.


In `packages/storage-postgres/src/schema/categories.ts`, the enum is named `document_state` but is being used for categories:

```typescript
export const categories = pgEnum('document_state', [
  'housing',
  'energy',
  ...
]);
```

This naming mismatch could cause confusion. Consider renaming the enum to match its purpose:

```diff
-export const categories = pgEnum('document_state', [
+export const categories = pgEnum('category', [
```


Also applies to: 15-17

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T12:59:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { within } from '@storybook/testing-library';
+import { expect } from '@storybook/jest';
+
+import { MultiSelect } from './chips';
+
+const frameworksList = [
+  { value: 'housing', label: 'Housing' },
+  { value: 'energy', label: 'Energy' },
+  { value: 'mobility', label: 'Mobility' },
+  { value: 'water', label: 'Water' },
+  { value: 'air', label: 'Air' },
+  { value: 'soil', label: 'Soil' },
+  { value: 'flora', label: 'Flora' },
+  { value: 'fauna', label: 'Fauna' },
+  { value: 'fungi', label: 'Fungi' },
+  { value: 'food', label: 'Food' },
+  { value: 'education', label: 'Education' },
+  { value: 'art', label: 'Art' },
+  { value: 'health', label: 'Health' },
+  { value: 'tech', label: 'Tech' },
+];
+
+const meta: Meta<typeof MultiSelect> = {
+  component: MultiSelect,
+  title: 'UI/MultiSelect',
+};
+export default meta;
+type Story = StoryObj<typeof MultiSelect>;
+
+export const Primary: Story = {
+  args: {
+    options: frameworksList,
+    defaultValue: [],
+    placeholder: 'Select Categories',
+    variant: 'inverted',
+    animation: 2,
+    maxCount: 3,
+  },
+};
+
+export const Heading: Story = {
+  args: {},
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    expect(canvas.getByText(/Welcome to MultiSelect!/gi)).toBeTruthy();
+  },
+};
path: packages/ui/src/chips.stories.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Heading story test will fail**

The play function is checking for text that doesn't appear to exist in the component. The test expects to find "Welcome to MultiSelect!" but this text doesn't appear in the MultiSelect component implementation.

Either update the test to check for content that actually exists in the component, or add the expected text to the component:

```diff
-    expect(canvas.getByText(/Welcome to MultiSelect!/gi)).toBeTruthy();
+    expect(canvas.getByText(/Select Categories/gi)).toBeTruthy();
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export const Heading: Story = {
  args: {},
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    expect(canvas.getByText(/Select Categories/gi)).toBeTruthy();
  },
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 2e19e4f
created_at: 2025-04-10T12:59:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { CheckIcon, XCircle, ChevronDown, XIcon } from 'lucide-react';
+import { Popover, PopoverContent, PopoverTrigger } from './popover';
+import { Button } from './button';
+import { cn } from '@hypha-platform/lib/utils';
+import { Badge } from './badge';
+import { Separator } from './separator';
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+  CommandSeparator,
+} from './command';
+
path: packages/ui/src/chips.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Inconsistent utility import path**

The `cn` utility is imported from `@hypha-platform/lib/utils` here, but from `@hypha-platform/ui-utils` in other files like popover.tsx.

Standardize the import path for consistency:

```diff
-import { cn } from '@hypha-platform/lib/utils';
+import { cn } from '@hypha-platform/ui-utils';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { CheckIcon, XCircle, ChevronDown, XIcon } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
import { Button } from './button';
import { cn } from '@hypha-platform/ui-utils';
import { Badge } from './badge';
import { Separator } from './separator';
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from './command';
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T12:59:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,466 @@
+{
+  "id": "acf1ea6a-a2ab-40c1-b3b8-4b3e68e22532",
+  "prevId": "61cc4ede-5bf8-4cea-830e-6865a9cbbe46",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.documents": {
+      "name": "documents",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "creator_id": {
+          "name": "creator_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "space_id": {
+          "name": "space_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "state": {
+          "name": "state",
+          "type": "document_state",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'discussion'"
+        },
+        "slug": {
+          "name": "slug",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "documents_creator_id_people_id_fk": {
+          "name": "documents_creator_id_people_id_fk",
+          "tableFrom": "documents",
+          "tableTo": "people",
+          "columnsFrom": [
+            "creator_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "documents_space_id_spaces_id_fk": {
+          "name": "documents_space_id_spaces_id_fk",
+          "tableFrom": "documents",
+          "tableTo": "spaces",
+          "columnsFrom": [
+            "space_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.memberships": {
+      "name": "memberships",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "person_id": {
+          "name": "person_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "space_id": {
+          "name": "space_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "person_space_idx": {
+          "name": "person_space_idx",
+          "columns": [
+            {
+              "expression": "person_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "space_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "memberships_person_id_people_id_fk": {
+          "name": "memberships_person_id_people_id_fk",
+          "tableFrom": "memberships",
+          "tableTo": "people",
+          "columnsFrom": [
+            "person_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "memberships_space_id_spaces_id_fk": {
+          "name": "memberships_space_id_spaces_id_fk",
+          "tableFrom": "memberships",
+          "tableTo": "spaces",
+          "columnsFrom": [
+            "space_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.people": {
+      "name": "people",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "sub": {
+          "name": "sub",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "(auth.user_id())"
+        },
+        "slug": {
+          "name": "slug",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "avatar_url": {
+          "name": "avatar_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "lead_image_url": {
+          "name": "lead_image_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "email": {
+          "name": "email",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "location": {
+          "name": "location",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "surname": {
+          "name": "surname",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "nickname": {
+          "name": "nickname",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "people_sub_unique": {
+          "name": "people_sub_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "sub"
+          ]
+        },
+        "people_slug_unique": {
+          "name": "people_slug_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "slug"
+          ]
+        },
+        "people_email_unique": {
+          "name": "people_email_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "email"
+          ]
+        }
+      },
+      "policies": {
+        "crud-authenticated-policy-select": {
+          "name": "crud-authenticated-policy-select",
+          "as": "PERMISSIVE",
+          "for": "SELECT",
+          "to": [
+            "authenticated"
+          ],
+          "using": "true"
+        },
+        "crud-authenticated-policy-insert": {
+          "name": "crud-authenticated-policy-insert",
+          "as": "PERMISSIVE",
+          "for": "INSERT",
+          "to": [
+            "authenticated"
+          ],
+          "withCheck": "(select auth.user_id() = \"people\".\"sub\")"
+        },
+        "crud-authenticated-policy-update": {
+          "name": "crud-authenticated-policy-update",
+          "as": "PERMISSIVE",
+          "for": "UPDATE",
+          "to": [
+            "authenticated"
+          ],
+          "using": "(select auth.user_id() = \"people\".\"sub\")",
+          "withCheck": "(select auth.user_id() = \"people\".\"sub\")"
+        },
+        "crud-authenticated-policy-delete": {
+          "name": "crud-authenticated-policy-delete",
+          "as": "PERMISSIVE",
+          "for": "DELETE",
+          "to": [
+            "authenticated"
+          ],
+          "using": "(select auth.user_id() = \"people\".\"sub\")"
+        }
+      },
+      "checkConstraints": {},
+      "isRLSEnabled": false
path: packages/storage-postgres/migrations/meta/0020_snapshot.json
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Inconsistent RLS configuration in people table**

The people table has Row-Level Security (RLS) policies defined but `isRLSEnabled` is set to false, which means these policies won't be enforced.


If you intend to use RLS for the people table, update the configuration:

```diff
-      "isRLSEnabled": false
+      "isRLSEnabled": true
```

Otherwise, the defined policies will have no effect.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      "policies": {
        "crud-authenticated-policy-select": {
          "name": "crud-authenticated-policy-select",
          "as": "PERMISSIVE",
          "for": "SELECT",
          "to": [
            "authenticated"
          ],
          "using": "true"
        },
        "crud-authenticated-policy-insert": {
          "name": "crud-authenticated-policy-insert",
          "as": "PERMISSIVE",
          "for": "INSERT",
          "to": [
            "authenticated"
          ],
          "withCheck": "(select auth.user_id() = \"people\".\"sub\")"
        },
        "crud-authenticated-policy-update": {
          "name": "crud-authenticated-policy-update",
          "as": "PERMISSIVE",
          "for": "UPDATE",
          "to": [
            "authenticated"
          ],
          "using": "(select auth.user_id() = \"people\".\"sub\")",
          "withCheck": "(select auth.user_id() = \"people\".\"sub\")"
        },
        "crud-authenticated-policy-delete": {
          "name": "crud-authenticated-policy-delete",
          "as": "PERMISSIVE",
          "for": "DELETE",
          "to": [
            "authenticated"
          ],
          "using": "(select auth.user_id() = \"people\".\"sub\")"
        }
      },
      "checkConstraints": {},
      "isRLSEnabled": true
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T12:59:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +19,22 @@ export interface CreateSpaceInput {
   slug?: string;
   parentId?: number;
   links?: string[];
+  categories?: (
+    | 'housing'
+    | 'energy'
+    | 'mobility'
+    | 'water'
+    | 'air'
+    | 'soil'
+    | 'flora'
+    | 'fauna'
+    | 'fungi'
+    | 'food'
+    | 'education'
+    | 'art'
+    | 'health'
+    | 'tech'
+  )[];
path: packages/core/src/space/types.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing `categories` field in `UpdateSpaceInput`**

The `categories` field has been added to `CreateSpaceInput` but is missing from `UpdateSpaceInput`. This means categories cannot be updated after a space is created.

Add the categories field to the `UpdateSpaceInput` interface:

```diff
export interface UpdateSpaceInput {
  title?: string;
  description?: string;
  logoUrl?: string;
  leadImage?: string;
  slug?: string;
  parentId?: number;
  web3SpaceId?: number;
+  categories?: (
+    | 'housing'
+    | 'energy'
+    | 'mobility'
+    | 'water'
+    | 'air'
+    | 'soil'
+    | 'flora'
+    | 'fauna'
+    | 'fungi'
+    | 'food'
+    | 'education'
+    | 'art'
+    | 'health'
+    | 'tech'
+  )[];
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export interface UpdateSpaceInput {
  title?: string;
  description?: string;
  logoUrl?: string;
  leadImage?: string;
  slug?: string;
  parentId?: number;
  web3SpaceId?: number;
  categories?: (
    | 'housing'
    | 'energy'
    | 'mobility'
    | 'water'
    | 'air'
    | 'soil'
    | 'flora'
    | 'fauna'
    | 'fungi'
    | 'food'
    | 'education'
    | 'art'
    | 'health'
    | 'tech'
  )[];
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T15:17:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,14 +30,31 @@ export default async function Index(props: PageProps) {
   };
 
   const spaces = await createSpaceService().getAll();
+  const uniqueCategories = extractUniqueCategories(spaces);
 
   return (
     <Container>
       <Text className="text-9 text-center flex mb-8">
         Explore hundreds of Spaces in the Hypha Network
       </Text>
       <SpaceSearch />
-      <SpaceGroupSlider spaces={spaces} type="Hypha" getHref={getHref} />
+      {uniqueCategories.map((category) => (
+        <SpaceGroupSlider
+          key={category}
+          spaces={spaces.filter(
+            (space) => space.categories && space.categories.includes(category),
+          )}
path: apps/web/src/app/[lang]/network/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use optional chaining for safer property access**

As noted by the static analysis tool, using optional chaining would make the code more robust against potential null or undefined values.

```diff
- (space) => space.categories && space.categories.includes(category),
+ (space) => space.categories?.includes(category),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            (space) => space.categories?.includes(category),
          )}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 45-45: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T15:17:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,14 +30,31 @@
   };
 
   const spaces = await createSpaceService().getAll();
+  const uniqueCategories = extractUniqueCategories(spaces);
 
   return (
     <Container>
       <Text className="text-9 text-center flex mb-8">
         Explore hundreds of Spaces in the Hypha Network
       </Text>
       <SpaceSearch />
-      <SpaceGroupSlider spaces={spaces} type="Hypha" getHref={getHref} />
+      {uniqueCategories.map((category) => (
+        <SpaceGroupSlider
+          key={category}
+          spaces={spaces.filter(
+            (space) => space.categories && space.categories.includes(category),
+          )}
+          type={category}
+          getHref={getHref}
+        />
+      ))}
+      <SpaceGroupSlider
+        spaces={spaces.filter(
+          (space) => space.categories && space.categories.length === 0,
+        )}
path: apps/web/src/app/[lang]/network/page.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix logical error in filter condition**

The current condition `space.categories && space.categories.length === 0` is a logical contradiction. If `space.categories` exists and is truthy, it's unlikely to have a length of 0. You probably meant to filter spaces that either don't have categories or have empty categories.

```diff
- (space) => space.categories && space.categories.length === 0,
+ (space) => !space.categories || space.categories.length === 0,
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        spaces={spaces.filter(
          (space) => !space.categories || space.categories.length === 0,
        )}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T15:17:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,6 +9,17 @@
   params: Promise<{ lang: Locale; id: string }>;
 };
 
+function extractUniqueCategories(spaces: any[]): string[] {
+  const categories = spaces.reduce((acc, space) => {
+    if (space.categories) {
+      acc.push(...space.categories);
+    }
+    return acc;
+  }, [] as string[]);
+
+  return Array.from(new Set(categories));
+}
path: apps/web/src/app/[lang]/network/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use specific type instead of `any[]`**

Using `any[]` for the `spaces` parameter reduces type safety. Given that we're accessing the `categories` property, a more specific type would be better.

```diff
- function extractUniqueCategories(spaces: any[]): string[] {
+ import { Space } from '@hypha-platform/core';
+ 
+ function extractUniqueCategories(spaces: Space[]): string[] {
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { Space } from '@hypha-platform/core';

function extractUniqueCategories(spaces: Space[]): string[] {
  const categories = spaces.reduce((acc, space) => {
    if (space.categories) {
      acc.push(...space.categories);
    }
    return acc;
  }, [] as string[]);

  return Array.from(new Set(categories));
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T15:17:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { CheckIcon, XCircle, ChevronDown, XIcon } from 'lucide-react';
+import { Popover, PopoverContent, PopoverTrigger } from './popover';
+import { Button } from './button';
+import { cn } from '@hypha-platform/ui-utils';
+import { Badge } from './badge';
+import { Separator } from './separator';
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+  CommandSeparator,
+} from './command';
+
+/**
+ * Variants for the multi-select component to handle different styles.
+ * Uses class-variance-authority (cva) to define different styles based on "variant" prop.
+ */
+const multiSelectVariants = cva(
+  'm-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300',
+  {
+    variants: {
+      variant: {
+        default:
+          'border-foreground/10 text-foreground bg-card hover:bg-card/80',
+        secondary:
+          'border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80',
+        destructive:
+          'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+        inverted: 'inverted',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+);
+
+/**
+ * Props for MultiSelect component
+ */
+interface MultiSelectProps
+  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+    VariantProps<typeof multiSelectVariants> {
+  /**
+   * An array of option objects to be displayed in the multi-select component.
+   * Each option object has a label, value, and an optional icon.
+   */
+  options: {
+    /** The text to display for the option. */
+    label: string;
+    /** The unique value associated with the option. */
+    value: string;
+    /** Optional icon component to display alongside the option. */
+    icon?: React.ComponentType<{ className?: string }>;
+  }[];
+
+  /**
+   * Callback function triggered when the selected values change.
+   * Receives an array of the new selected values.
+   */
+  onValueChange: (value: string[]) => void;
+
+  /** The default selected values when the component mounts. */
+  defaultValue?: string[];
+
+  /**
+   * Placeholder text to be displayed when no values are selected.
+   * Optional, defaults to "Select options".
+   */
+  placeholder?: string;
+
+  /**
+   * Animation duration in seconds for the visual effects (e.g., bouncing badges).
+   * Optional, defaults to 0 (no animation).
+   */
+  animation?: number;
+
+  /**
+   * Maximum number of items to display. Extra selected items will be summarized.
+   * Optional, defaults to 3.
+   */
+  maxCount?: number;
+
+  /**
+   * The modality of the popover. When set to true, interaction with outside elements
+   * will be disabled and only popover content will be visible to screen readers.
+   * Optional, defaults to false.
+   */
+  modalPopover?: boolean;
+
+  /**
+   * If true, renders the multi-select component as a child of another component.
+   * Optional, defaults to false.
+   */
+  asChild?: boolean;
+
+  /**
+   * Additional class names to apply custom styles to the multi-select component.
+   * Optional, can be used to add custom styles.
+   */
+  className?: string;
+}
+
+export const MultiSelect = React.forwardRef<
+  HTMLButtonElement,
+  MultiSelectProps
+>(
+  (
+    {
+      options,
+      onValueChange,
+      variant,
+      defaultValue = [],
+      placeholder = 'Select options',
+      animation = 0,
+      maxCount = 3,
+      modalPopover = false,
+      asChild = false,
+      className,
+      ...props
+    },
+    ref,
+  ) => {
+    const [selectedValues, setSelectedValues] =
+      React.useState<string[]>(defaultValue);
+    const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
+
+    const handleInputKeyDown = (
+      event: React.KeyboardEvent<HTMLInputElement>,
+    ) => {
+      if (event.key === 'Enter') {
+        setIsPopoverOpen(true);
+      } else if (event.key === 'Backspace' && !event.currentTarget.value) {
+        const newSelectedValues = [...selectedValues];
+        newSelectedValues.pop();
+        setSelectedValues(newSelectedValues);
+        onValueChange(newSelectedValues);
+      }
+    };
+
+    const toggleOption = (option: string) => {
+      const newSelectedValues = selectedValues.includes(option)
+        ? selectedValues.filter((value) => value !== option)
+        : [...selectedValues, option];
+      setSelectedValues(newSelectedValues);
+      onValueChange(newSelectedValues);
+    };
+
+    const handleClear = () => {
+      setSelectedValues([]);
+      onValueChange([]);
+    };
+
+    const handleTogglePopover = () => {
+      setIsPopoverOpen((prev) => !prev);
+    };
+
+    const clearExtraOptions = () => {
+      const newSelectedValues = selectedValues.slice(0, maxCount);
+      setSelectedValues(newSelectedValues);
+      onValueChange(newSelectedValues);
+    };
+
+    const toggleAll = () => {
+      if (selectedValues.length === options.length) {
+        handleClear();
+      } else {
+        const allValues = options.map((option) => option.value);
+        setSelectedValues(allValues);
+        onValueChange(allValues);
+      }
+    };
+
+    return (
+      <Popover
+        open={isPopoverOpen}
+        onOpenChange={setIsPopoverOpen}
+        modal={modalPopover}
+      >
+        <PopoverTrigger asChild>
+          <Button
+            ref={ref}
+            {...props}
+            onClick={handleTogglePopover}
+            className={cn(
+              'flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
+              className,
+            )}
+          >
+            {selectedValues.length > 0 ? (
+              <div className="flex justify-between items-center w-full">
+                <div className="flex flex-wrap items-center">
+                  {selectedValues.slice(0, maxCount).map((value) => {
+                    const option = options.find((o) => o.value === value);
+                    const IconComponent = option?.icon;
+                    return (
+                      <Badge
+                        key={value}
+                        className={cn(multiSelectVariants({ variant }))}
+                        style={{ animationDuration: `${animation}s` }}
+                      >
+                        {IconComponent && (
+                          <IconComponent className="h-4 w-4 mr-2" />
+                        )}
+                        {option?.label}
+                        <XCircle
+                          className="ml-2 h-4 w-4 cursor-pointer"
+                          onClick={(event) => {
+                            event.stopPropagation();
+                            toggleOption(value);
+                          }}
+                        />
+                      </Badge>
+                    );
+                  })}
+                  {selectedValues.length > maxCount && (
+                    <Badge
+                      className={cn(
+                        'bg-transparent text-foreground border-foreground/1 hover:bg-transparent',
+                        multiSelectVariants({ variant }),
+                      )}
+                      style={{ animationDuration: `${animation}s` }}
+                    >
+                      {`+ ${selectedValues.length - maxCount} more`}
+                      <XCircle
+                        className="ml-2 h-4 w-4 cursor-pointer"
+                        onClick={(event) => {
+                          event.stopPropagation();
+                          clearExtraOptions();
+                        }}
+                      />
+                    </Badge>
+                  )}
+                </div>
+                <div className="flex items-center justify-between">
+                  <XIcon
+                    className="h-4 mx-2 cursor-pointer text-muted-foreground"
+                    onClick={(event) => {
+                      event.stopPropagation();
+                      handleClear();
+                    }}
+                  />
+                  <Separator
+                    orientation="vertical"
+                    className="flex min-h-6 h-full"
+                  />
+                  <ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
+                </div>
+              </div>
+            ) : (
+              <div className="flex items-center justify-between w-full mx-auto">
+                <span className="text-sm text-muted-foreground mx-3">
+                  {placeholder}
+                </span>
+                <ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
+              </div>
+            )}
+          </Button>
+        </PopoverTrigger>
path: packages/ui/src/chips.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Enhance accessibility with ARIA attributes**

The component is missing ARIA attributes that would make it more accessible for users with disabilities.


Add appropriate ARIA attributes to improve accessibility:

```diff
 <Button
   ref={ref}
   {...props}
   onClick={handleTogglePopover}
+  aria-haspopup="listbox"
+  aria-expanded={isPopoverOpen}
+  aria-label={placeholder}
   className={cn(
     'flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
     className,
   )}
 >
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    return (
      <Popover
        open={isPopoverOpen}
        onOpenChange={setIsPopoverOpen}
        modal={modalPopover}
      >
        <PopoverTrigger asChild>
          <Button
            ref={ref}
            {...props}
            onClick={handleTogglePopover}
            aria-haspopup="listbox"
            aria-expanded={isPopoverOpen}
            aria-label={placeholder}
            className={cn(
              'flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
              className,
            )}
          >
            {selectedValues.length > 0 ? (
              <div className="flex justify-between items-center w-full">
                <div className="flex flex-wrap items-center">
                  {selectedValues.slice(0, maxCount).map((value) => {
                    const option = options.find((o) => o.value === value);
                    const IconComponent = option?.icon;
                    return (
                      <Badge
                        key={value}
                        className={cn(multiSelectVariants({ variant }))}
                        style={{ animationDuration: `${animation}s` }}
                      >
                        {IconComponent && (
                          <IconComponent className="h-4 w-4 mr-2" />
                        )}
                        {option?.label}
                        <XCircle
                          className="ml-2 h-4 w-4 cursor-pointer"
                          onClick={(event) => {
                            event.stopPropagation();
                            toggleOption(value);
                          }}
                        />
                      </Badge>
                    );
                  })}
                  {selectedValues.length > maxCount && (
                    <Badge
                      className={cn(
                        'bg-transparent text-foreground border-foreground/1 hover:bg-transparent',
                        multiSelectVariants({ variant }),
                      )}
                      style={{ animationDuration: `${animation}s` }}
                    >
                      {`+ ${selectedValues.length - maxCount} more`}
                      <XCircle
                        className="ml-2 h-4 w-4 cursor-pointer"
                        onClick={(event) => {
                          event.stopPropagation();
                          clearExtraOptions();
                        }}
                      />
                    </Badge>
                  )}
                </div>
                <div className="flex items-center justify-between">
                  <XIcon
                    className="h-4 mx-2 cursor-pointer text-muted-foreground"
                    onClick={(event) => {
                      event.stopPropagation();
                      handleClear();
                    }}
                  />
                  <Separator
                    orientation="vertical"
                    className="flex min-h-6 h-full"
                  />
                  <ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
                </div>
              </div>
            ) : (
              <div className="flex items-center justify-between w-full mx-auto">
                <span className="text-sm text-muted-foreground mx-3">
                  {placeholder}
                </span>
                <ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
              </div>
            )}
          </Button>
        </PopoverTrigger>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-11T06:55:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,12 +3,24 @@ import { Container } from '@hypha-platform/ui';
 import { Text } from '@radix-ui/themes';
 import { SpaceGroupSlider, SpaceSearch } from '@hypha-platform/epics';
 import { getDhoPathAgreements } from '../dho/[id]/agreements/constants';
-import { createSpaceService } from '@hypha-platform/core/server';
+import { createSpaceService, Space } from '@hypha-platform/core/server';
+import { Category } from '@hypha-platform/core/client';
 
 type PageProps = {
   params: Promise<{ lang: Locale; id: string }>;
 };
 
+function extractUniqueCategories(spaces: Space[]) {
+  const categories = spaces.reduce<Category[]>((acc, space) => {
+    if (space.categories) {
+      return [...acc, ...space.categories];
+    }
+    return acc;
+  }, []);
+
+  return Array.from(new Set(categories));
+}
path: apps/web/src/app/[lang]/network/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve performance and typings in the extractUniqueCategories function**

The current implementation of `extractUniqueCategories` has two issues:

1. Using spread syntax (`...`) on accumulators in a reduce function causes O(n²) time complexity, which can be inefficient for large arrays
2. Missing explicit return type annotation


```diff
-function extractUniqueCategories(spaces: Space[]) {
-  const categories = spaces.reduce<Category[]>((acc, space) => {
-    if (space.categories) {
-      return [...acc, ...space.categories];
-    }
-    return acc;
-  }, []);
-
-  return Array.from(new Set(categories));
+function extractUniqueCategories(spaces: Space[]): Category[] {
+  const categories: Category[] = [];
+  
+  spaces.forEach(space => {
+    if (space.categories) {
+      space.categories.forEach(category => {
+        if (!categories.includes(category)) {
+          categories.push(category);
+        }
+      });
+    }
+  });
+  
+  return categories;
}
```

Alternatively, for a more concise solution:

```diff
-function extractUniqueCategories(spaces: Space[]) {
-  const categories = spaces.reduce<Category[]>((acc, space) => {
-    if (space.categories) {
-      return [...acc, ...space.categories];
-    }
-    return acc;
-  }, []);
-
-  return Array.from(new Set(categories));
+function extractUniqueCategories(spaces: Space[]): Category[] {
+  const categoriesSet = new Set<Category>();
+  
+  spaces.forEach(space => {
+    if (space.categories) {
+      space.categories.forEach(category => categoriesSet.add(category));
+    }
+  });
+  
+  return Array.from(categoriesSet);
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
function extractUniqueCategories(spaces: Space[]): Category[] {
  const categoriesSet = new Set<Category>();
  
  spaces.forEach(space => {
    if (space.categories) {
      space.categories.forEach(category => categoriesSet.add(category));
    }
  });
  
  return Array.from(categoriesSet);
}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 16-16: Avoid the use of spread (`...`) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in `.reduce`) because it causes a time complexity of `O(n^2)`.
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit a268343
created_at: 2025-04-11T06:55:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-10T12:38:49Z
body: [vc]: #9B4Vs1Bq4fPzGh5vhmW/qC9fv+0VZLscEk14GhQbH6w=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3d3ctaHlwaGEtZWFydGgtdjMiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vaHlwaGEtZGFvL3d3dy1oeXBoYS1lYXJ0aC12My82azhFNnZpeTZLS2FrUTF0bjJweW1yOUNOV3p5IiwicHJldmlld1VybCI6Ind3dy1oeXBoYS1lYXJ0aC12My1naXQtZmVhdC02OTQtcGxpdHplbmJlcmdlci0wNDI5NDItaHlwaGEtZGFvLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoid3d3LWh5cGhhLWVhcnRoLXYzLWdpdC1mZWF0LTY5NC1wbGl0emVuYmVyZ2VyLTA0Mjk0Mi1oeXBoYS1kYW8udmVyY2VsLmFwcCJ9LCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **www-hypha-earth-v3** | ✅ Ready ([Inspect](https://vercel.com/hypha-dao/www-hypha-earth-v3/6k8E6viy6KKakQ1tn2pymr9CNWzy)) | [Visit Preview](https://www-hypha-earth-v3-git-feat-694-plitzenberger-042942-hypha-dao.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/www-hypha-earth-v3-git-feat-694-plitzenberger-042942-hypha-dao.vercel.app?via=pr-comment-feedback-link) | Apr 11, 2025 7:39am |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-10T12:38:57Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces a categorization feature across multiple modules. It updates package dependencies and adds a new "categories" field to space creation forms, validation schemas, and the PostgreSQL database schema. New UI components (MultiSelect, Popover, Command, Dialog) and their Storybook stories are added to support category/tag selection. Additionally, related migrations and schemas are updated to integrate this feature.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `package.json` | Updated `@radix-ui/react-dialog` version; added dependencies: `@radix-ui/react-popover` and `cmdk`. |
| `packages/core/src/space/validation.ts`<br>`packages/epics/src/spaces/components/create-space-form.tsx` | Added new property `categories` with Zod validation and integrated a `MultiSelect` component into the space creation form. |
| `packages/storage-postgres/migrations/0020_wide_johnny_blaze.sql`<br>`packages/storage-postgres/migrations/meta/0020_snapshot.json`<br>`packages/storage-postgres/migrations/meta/_journal.json` | Introduced SQL migration adding a jsonb `categories` column to the "spaces" table and updated migration metadata. |
| `packages/storage-postgres/src/schema/categories.ts`<br>`packages/storage-postgres/src/schema/space.ts` | Added a new categories enum and integrated a `categories` field into the spaces table schema with a default empty array. |
| `packages/ui/src/chips.stories.tsx`<br>`packages/ui/src/chips.tsx`<br>`packages/ui/src/command.tsx`<br>`packages/ui/src/dialog.tsx`<br>`packages/ui/src/popover.tsx`<br>`packages/ui/src/index.ts` | Added new UI components (MultiSelect, Command, Dialog, Popover) and corresponding Storybook stories, plus an export update for the chips module. |

## Assessment against linked issues

| Objective                                              | Addressed | Explanation                                                |
|--------------------------------------------------------|-----------|------------------------------------------------------------|
| Add a tag field in Space Settings > Configure Space (#694) | ✅        |                                                            |

## Suggested reviewers
- evgenibir

## Poem
> I'm just a bunny with a happy tip-toe,  
> Leaping through code where categories grow,  
> Adding tags and fields with a gentle click,  
> MultiSelect magic that does the trick,  
> Hopping along with every new line of code,  
> Celebrating changes on this fun rabbit road!  
> 🐇✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 56461d08dc982b4947f85b14b58280e7ea7388e7 and e8f29a5faa2eded399ebe31c58220bd3fa96928e.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/storage-postgres/src/seed.ts` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/storage-postgres/src/seed.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: deploy-preview
* GitHub Check: main

</details>

</details>
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQ4rjedJAARABmJNQAFGYAbACcACwAlFy4aETIcfh8iNxoDNIxkADuaMiI2ALM6jT0cuGwntiIlDze6gBeZAKUpHyY9KjwShjicfDRHbhdkBhOo3wA7ACMWxowq7zwzC7y3NgU3Pi9/HGdqBe+/kEhiGGoBOi0tAGIyEhGp51sxNpAMpkADToAEYWjwBjUJbyFaeJSIBgUeDccT4LDxeAAD0gaSyGmqHi8zwCwVCjEwkFG6AYlX+0RWFHw2CIsE6nmKvnwNXgGCIkAAqjYADJcADasFwuG4iA4AHpVUR1LAmhomMxVbBZNxYGgwLQ0PgDUaTWAaiQBKqnt5VbstgBdJIKpUq9WalY6vVW42m82Ww3B232x0+Z2u7IaIz6YzgKBkej4e5oPCEUjkKhtBSsdhcXj8YSicRSGTyJhKKiqdRaHRJkxQODTZAMrMEYhkZQFvVsOZcKg1SCNZyuRk1xTKBuabS6MCGZOmAzlBgAawKJA0QkQeI4BhiJ4MFkgAEEAJK9vNI+gT05TjOME2i6TuVYMN+kWF8yAAAYbtupB7geGAAZACxRCgGAMN42BKOgWDYNw5o0OE+D/lIFCIPAeJ3P+AEAAJUPChJgNg8CqgEFS4Ga8BoN4+BEJBwE7lBnLMIBAB6OwaPxmSQV8AF8QJGipAB0IivCiLiKK6CQC0GAlJAOF4QRqFEGRu6Xj86j4RgTHeLI0JoGsJBjko3BpmQDCmYBpFoORlHUbRYhgNc1w4VJkAmjIJBkN8Sj0MKKzYZQGlYKJ/H8VsAEHBeKmonw5BWSQNlwnZDkAQwzC0Juvn+eg3gHoygVYCKuCcrQ2CVKFWoRbhhm8bFAkJYc0ieMwijwAscmGcgARxFEYiQKh6HSJhkAkISSDyWK7G/sh9CorB8E9C1L5pTwFQgVNIr/rw+AiGIADkyDWbZcGLIgCZnpYF7eDQ+aDdNa1KPBLjUG9L6zdcFAFqpFwCAMDAzXMBkfgYUAACIZdd9lqZFLUTfeXAkWRRKuTRiQefCTEsZBcRca14lCdNMXiZJsGAUtu77niAEw5AABylmQFdWVwfIzkhRjTkuVRuN0Z5+DeZQkFhby6ktVTcWQYdQF7TuYFMyz7PpZlsxI3zdAY3lBVS41ssEfL7W08rW6q4zEHHqeMNrvTiCqkwASqogFAMB7G4kKqEhMTMP14houAqvbMQPZeN65v20SPmchHfpgv5GBeFljsdNmAzlckkEQJS3UV9TlUFeurVha25bRND2L7ADq9oAExWJyyqQYIp2aIcjxt5QuDyKgSgLOQ9Al12FBUPIf0gnHD7VSKhRmYKwoKWUoh9Qi9I0AXWJTQE3j3tNZQVJ4mKJLiGDSXBCHwgp4uXw0dW8iXZ2wFyeGimd0JnX2FBELIb+kAzo9TUAMAeQCzp1BepA7QFBIEHngN4SBI0ShoBQVmIyKDsCingCg/AihIF0DqsHDAsDAaQK6ExFYQDJjAJoN+M6BxKRZ37vIYq5lh5ZmempJiIRCIMhIMwHEvNJ5oFkMw1Ys15qLx4H3QGt1LbnyRHXU+jcBAtzbogSCARTiHRwcnd8tAzJwn/PgHCxlxzVTqrgS4ngXxrU7hWBQkN1hTS+I0bgAMwhrQDgMdCW17gByxB/dAiosQCDwPvEgh8CweN9owGuhl7rniei9UhyAvgfVEIfV6eJkB/UJN46IwMmhgwhhEW6iZrDyIHsFfWgE8671ulwQYWgxGyCSG0sgTgkiyg0AMt02R4xcJ8LgPpQzFbRWUbXBuzdW7i20ZbZ2rsSh+09t7E+lR/aBwCSHMOzMTyR0dmAIwKyMoIhdhsn2p8XZ6muOQOYdya4kDAFs15xQKDMFDogQkR4jlR2vLeOe44nBPmnvcQxqcDCUihQdOYNU6qeHMjtXKSJmnSGJosbwUwsBVwAMIvNUZUAAYiUZgkF7l4nYNfAygdhhj07FgIRIj0AdNptkuI3Cwh+LeIlQCABZMZ8B7AxIrJSlgDz2AoE7D8dkWFjJCnGr0XCx8xVjWYMK7gMEmmFymiTFgileB0BICPaIAx3jQhqAgb861b5TQfm9RotrX4AAkP6L0gQAUTzAAyBArBBIPUIAkx6YUp3U6oK4VorRq4EVrCHe+ZojS0OuZACZKvkkuxbQXyeAg2DFkVXT53ymAIvwN4Ymql3hIiUpgHcQ5u5XjCGjGgmTVjFqsTWjkXIeRETxAANT4SQfFP4SCQTfLQKIExTHwjKIfWQTL5B+KDpfJS0hEA7mQJtBSVcM3MAFeuncErhHUrmIlfSl9jIOTWicbxnaaANqgqpACQrnoivVXGmV9TjG1AQDBG9J7AZPr4DYPGHwhpCPMXQFJj1nr9jelk1Yn1ckZMIv9EoQM+Ag3KewKGiBqmaygtm79Bt0V6qWYdTl3LeEIQdfcAlRLfZ7uPVKuYyyVa/lVBchgVyvY3NZKsk9jyw6uxeW832YBi0/MJMzKAhGqXCZI4BYAr7xAxorHoKZRFCUXxIMSkgzHCysY+NFc53BLkez4+8u5krT0iZma895knyXScOQ7CApz1wcekB7AgVBSBi3eNpHzLRtIZNVAABgi03CLAB9YUShYtCHwLADAGBZCxdBmgYYGhECBG8P808qSY5/yPgnZ8kLR34YMOnHa9gACKUolLwDC6u4qowgpVURfVTo1ASovTbZ4GI1nqj5FBp4DofNZEoo5kwBCzAsBGTYPQGIuq96IBiJI1Ac2nCVQKfRo0nhbYCBWn5cenNTXUd5fYzMzLhF1IAFL2AAPKszZVPSASQzqyjdGdDgHBjvxnMLB9Jj93pIZyd9MHhTinpiw2UreuHxDQygPi8tu2lMAVW2R9b1RjuQFhl6klF5xRSmgMAn7f2AfgQEFpsbMEscja01bfaVy/M7kC7gYLLtQt5IwC7KLMX4szBIEllLaWMtZZy3litEdExO282ztBAXrhBb+KqXn4W2D5Ei9FuLiAjLKnfpoW2hXI7FeBUmh8YLE4vjhdV2rHMnuvea61lq0FPDtYqrBaqigkX0E+f+KwNwucBAa019CKh6ieAxF0U40lZjzEWIywCgv9eG8QMbtWEEttFCQWiU1Ip3GrHeBQGxdjgNPsuOEFQUQVSAVoPgBgTh2DaOhABNgoJIoIHbu3my4soi+ToQBazvk8a8mlnyeAfA/g2TEJIM+6OFuIGhJH8Ih2V8uNL9oJ5oaXixMGj3u6n5PAAUb83htSz6eeBFBtdECh5v89BS65AAEZi+WrhfEosX3/t/eT/nNdvCIQfdvdETEbES+D/atGgKAhCVidvBzWgWLagIfUxACFtOgZAuNK1RqN/HNGEI6LEcFSATcEgCRSAJtGaatUGJALoIoNZFrLAUg+QA+DJI/WoRqKufvbVMdU7EfX2K/WvaQSRU/aA3gnbBbM7TsC7LlMZGjfhF8ACM6WdZvf4QyM6BKE/QCTvTYTPbEQQ8bLfeafbQCX/OmSKPEAAqA32KwhAl5JAlAvfdAtCe8LA3ySfPAyCEuNaY4Yg5gg4Sg4eIveggIRgkgsgrfaqHfMOSAAYDATcWRICCwjAKw8HU/bgwfGvQw4ff/d/NIwCUfLIqIRKSqLKYkbbew/gPFL8FgNQIyVdRQ7OcCVInImwvIr4MgRoAIcaDAeAWkcgf4e6SkJIgfXg6/KCC+OxZACQp/Z1F+V/MwkfJoWA7kD/NAAOfICgWLS4CtdvKIZyH/U4UgbYigXYhvaQcAnEQyD/IRbQM4gCZiAaJmdvJbMdP/S4V4j/XorcT4uw3TBw7AvgjAgE9w3AvI7wo4IgxOfwiUXo2kSIqgKqTsbotALxAYeVIiRoWnP/OA1A+gACW4pBCVR/CNSkcY2/e1BoEgCxPwa4MGRRQPfFGwcUWGfgbODJaEToy4QtE0MIPEEyHAVESGPOegHoSKekLAbODtOiN6AQaPdMaokgafAo5Y+Q3cLQ/g25YSIQu1RCKaTwvYlifAE4+4/YpAk4I9IA9QEA84jELEK454go3E9vO0AQAAZli1yMAMAjiM3Db0aRxyLj+NcMcKBJcLaDcJwPCk8IIJ8KhKnBhPThwT6P4VLW3yqi/VRO1WT3yJH1xIf123ugvAvUMivRMQhhbz53XxsgbybxbzmE9PyBgK/SCNHg4PCmu1fxUJ6Cig/yzhuCYjxMAgKACCEXYA6kpE11XQ905nwCmmSl1KQkwHkF6H6NZGhE5Dr2hFUgkEWBqGPyMEBTgz50yUrghy+hPLQyKQwxKXhxoPBiRyqQ1g5nGNbIaSAgRwYA0HP3rIOXbN5BmIWO9M/2oG/0WK9I/2ALeNtMuMgL/ybOgrzNWODIjNDOcMmhBITDkxfJ1LfNoAxmwwRA0B0O730ONnCkAtMOAqaMsPAraOAsQMjMAmBLcKwrZhwsMLwoIs/I0AyN4Mn0ooNJVOxKdOQuHI2JcBNI/zNMOJ3CktAIuPtLgsAkJNNKb1IS+LQDYCgI+K0sQu+M3F+MaXsKYvQpDLjTYsI1fML1Hm4vvNywEPIoAqXyfyEoeKNPkp9MSHNKOMQqgo/zAKUuuJxLEoAldI9Igr2JFD9I/zWyDOMv+NMpYpQMso5h6W4i4rph4p/IbUbKRCcrVK7KQFUN7L7zbgHPuJHMCgbU0IBROTOUV182V1eVV1DxCxaxPI1xIB1zF10u8GzzN0BRKzvALHKwhVfBTmhkdzHHYCnC9zLjlQrn/BiFmtumqBcA+0o1WGdze1L3L26IcVWCnJamSz6pEO/QqTmohP5HLWYlXjFGoAXkiVbS4AZBklmk+zwJk2yEIhizMmRmagIhfBiC2BiH+vEDYGrWEU+utTIAAh+pfD2EyEyCbgAA4ABWCLVIJudG1G1G8GgoQiGINPYXRLZLVLdLTLQ+YYMG07NNAQWiTca4JEyCa7ccbq96MvXSdsWVeEVdWaGgOEQbGaOad4WRC1Pk+4Y6giVaqaaWLkMIahSgabdLEWmRBSWWgpPgVEZUx6iJKJA81JY81DRDAvC81DGHG8uHLwe8ipPDAjNKhFXmRajGAAbxiBmEJBiC4D+tiFNgwG9tiFBuhBiFhoDq4CRpRoxqxpxrxpDvyCIEDuJr11JtF3JolypuyxIFppiAZsSCZvwCRMDusRIAAF9mcVl3hmrOducNcOqtduq0BVReqKAjJ+rbY3NjkPMGrrZOMq7/MWqQ9a7rlY9bjXZAzhCw5BqLdY4rdQVJxxr7c04M4iMGc4rJ6ll5rSjfdaoestr0jGr+6OdWrh6rNvxbjIJ4QAgxAShyCebV7PB0NAZpC0z8g2NXj8T8ULxoAvUABxZ7GwK8L1ewdwm1XkVACeD7F8UvReCDY1XoVxMUYJfCHoWci/aVMQzfCkxCWRN+D1L+H+P+P1H+UBINCBH+aBSgWBafBBQu5BH+VBKgDBHBdBBhnBTUfBQhQh3e0hchXAShRIZ6WAWhUxM6BhYR89Pm0s3wBydQEW7xJlfAFKSIzAMID+gMnecjUBhEcBy6Gy6IbdMULgogH1JwYmAxac0mKuX4eAQYQYKIMAclR0IgMAN2XgnqWqKIaEGZFWistgKsuU3oBU7Tb+v+gBoBkB97cRXPKxMvMQCvLlBgMhpEYWuKwYUhQiHK1vZkN2O+Ixs8rqORE1M1eeFJmDS8Y2sHU2i7c26He4J+zDG2nDSGZHarVHfJN+xWl2lS68oDV+yAL+n+/+wB4Biuw+9nFXIe9XEe8+04cezR9bH5WTAZjp1RzHBpyI7efOLR9jXunzI+yZtXfZs+uPJu9eu6A5OXeqrzPZpXAemu6Zk5se95H5aex6YakFMapOKrLQ+3H3brZFFehYGJegdRtFBZouXMwo8Y0e04DlVYTUyoJZh+mJ4FnFFs/RlPcyHagAIWrORVMVQH+grAxPfnRcgfEUIk7IuyxCkADysYRfSsHVoyWUOpv0AwLHBe2fW0gg8Z8G5oRfOaxRBa/RFDpQGAZX/MUlGR4TZpfEEXu1ESnnOqmwaPowQDzxFf+kmGFrXw8ZiWmgqEHEUBrTSYycD2s05IwDfCSZ3RL1mfQWmNRJUDIZRCwn7u6jWVif2uiEjyGNWFvQw3vTHLY0Dy5YxU3vHlKiwg6ywHLmmkSeSYwhWFQB2jRdoAuhkLFcvnKbSXg3yXyOQyh1+nqd6cacIofJaafOwrHHTcx3Oa4GOy+3ObOnjAABIB4bJgALwOlgAkhO2SA7cJ67omWh1EBshZQQRNg3Q9A9AkgRlLsxkJlsg6cEXoWdTYXzIlZK6JnB6jneNNkHX+NdxLm6ru6bnWdVRhZrlvx9Dcs/NbppM3no5LcysbcKsJr3wHcV77A/NZABACFNwH7K8q5VN31Y0WM7MpDS5t6AXcU6ZGrr2+Nb3lR73yNXNUX89Fzi9gRRBD0pxA3n7K9f3b6AP8BNxoQ5iCCX1G6vC0CSPXBnthAh9mIFJJ9W0FpxpxBwEGTSYSJ+7/3APVQOPF4wABgGazg6P8TiIBOyPNxVQRB3gOp04JbCJHV8l28SY9KagSg/SpR5pfIh5MXvG8R8gxWFJkHQlznYj4BSDgF3VDHvVfUQ1gEA0wFg1IF64kR4Ef4LwaGf57A6HIESVmImGf4SVMFWHgESV2G8FwuCEM2f4vUeHL5IEe3+Gf5XVBGaE98zpoBRBhGrUwGODngmQxSU9ehY0dT1AhE/xQPo0P1IPhN/XupG619A96u311MxAmvpVUBehm1uApXzIoLCIzpxQrxVQwPuuMvTs1oFM+uw4Yl7h+u59N5oNLwrFb61g9L8TW4LTXB40s3R5oRjpdy8mZDqMXAiBfyQiiJpvGvr478eT7EHSn8DVuIAItO2AdOKA9ODPyyWU6kZWeUh1/rtVT4yW6x/rkHVHyzMALTV1ao+c99zJTg5pNVuImAcFJb2bY1BpzqgJ51zG4JpzVIq59vwVIIBPXxRA/SQPISuo4IbsjpD5Kgoe+gaBCQwgYgZuBnh3qh96XgsoAh6AFu2NDH/wROFJxOqBXB/XU2OYZyyAbWcO0HfyMm6FpexRERyh3PWnCJOu1NGujMoOU1FSHA/25OIZdzOQMAG17pgcKnQcEMCmamUM6n5Grb+A7zmnKkUd2KxwGPBPyO118g19y4DYOmw+0AuAD18hgAB2XwHvY1NNdnL2kPvYUO7p+7H2w4ZNnyxwafI+em71+nKezguBg+xnbmr3qIb2j80PFn8/O75dPMVlM/XZG/8/n2gVZ632F7vnJrv3UUU/xVTfFMt7/m/c96LeWdN06/LMs/u/fksViie5pjbNFNFV9zlVxSPEP0lItUYJ1P3vSZOE25G8aht7KBEmSBoQ80JWXvGlD5/gwBYfmewBux34sQB5K0+BVCBAFoOk0MLvBZAdec6uL0VosgMoMRCluNVP5lgu4m+cfC4jM69EFI5kQ+KMG8DQhrsqPLAKfyYgoBS0BwDUmPx661ojIy0dQAmkoBt1g2leSriS3oDUsteqwXcnhH17jUPol/IUJKXFhQYKAARMIGdxFwv1jIcpLcFBAsaGR28A6IdCOkmraNbU0wDABIHI7Joug5AHCP+GYFiBog1LOFJAK359cb4epZAMwQA4uB6A1JaVBOjiJEArWteWRGKVVRfBxYQUHwoIN0HsdVgPqF6OEV5imJdEUGf8K/zCD6CCwbNSXjiz2gvMjA4oFVE6ywDwREgpQD9INGhAEAiARAGCJYlP6b46EAwFoD4lWBTs+gL4WdBD1kDxwP00QGrswFJI1EhMGDVCAo0UgeNiBXkIQUpFnAblQgWIefApCqjKB58BESfArTwhIQxUl+A4DNwaEPpkSngTgdgCvQvB4G7AaICXDlK0BloKwPrIiCwBMgZIkgGYGsJkY+8H8+PKQAKTSEuANulIX4OLCv6lE78U0cyL0BcCqCMAFwMIIHmggvRZEhQ07NUPnTSFIaG6UgL+iCgqQXgjQZ6MsKrxwgWuUaLribygElQyoO/O7qq2kaMAegQA2xhk27TcheQWcJ+C/nOKyFnozLEIH2TZ4kAOeFANYr0VODKUO8aAQkGjhx4f4uh3gYPBLBZHt56gI6JBN6WHxfR/grMPSmPmtaYBbWYoWgVBCiBzRuBleSzqgzFL0heghtEHPmyfzVMi2l5S2oDFvJNNEcVbaGABBtGF8J+0qEvi+ga4Qd0+C/Tvtn1cx2jRhFAO/pjgoG4AFk7cV0ZxndEr8C+No1vtcw7719kOLAU4CiJ75y4Z6pWUau+0Xo/MasQLLDrlDjGTBXM0HWNtP13p0BpIwiKIA2mmyFh4xuKF6L6Ml6gY6I9op5OdT0SpDTBcwdvGjmcBwhDO0hGoFQC8R9AXAXINAl2OrGU8DIUgYmAy1PyGxCoNnCTq4FO6chzulYsAVEDF65jCWvwvAD7lv6nwRBXHfNB8MAiwxGIzEViE2J8RYQHMnQk1n4AJgXiGei+bsTWP3GVArWuvBEUfCF4AQxxkwM8YTEvFQCyBMKBFv+LhBXgdxn6TEUZzwhEA2yXwI1rAL358ARQfw6EAkGoDclMB7Nb4eA1LSnZ1x0QQPJ8MSDMA68W6FVPI2Vp2QBWp+CCbQH05KcrxnMJADUOkKqc7cW49MG903zWpsxjEr1IqzYgrjxBikLlL4CkFAcJuMIxbDeJ4lIj1idxIQqBOGKMTf6nIVCB3H/gI9hgEGWJNEEHB0JGhRQ0cTxNFTlB8wJQVmkgAuEClegVklJpAG0hchlQScRSYT0YlNohEOiDKH8GyanDzuFwqsXQkKFRleQ642RBa3qFjwZ0SAIQqU1bSSMDIeIMsoBEYn2Af+uAZvJ+ggZHjwEM4VoaG1UhgjxEsiKwfgBsHjhspuUzsP8CbyMQCwAlTyZADTjPAoBW6bjrY08BVwGAEEAqcGkrylopGaUvwFKOQCvEqB9aGlBDC6KyI0y80DBgPEcHMhOQ/wf8MZO3G1iDxkaZXmW07AdT2xMRQPNqMOhKMugWGFwDETZboA0SCIUhI7yPIu8C2RoyHCaNLaw4rhFbO2q0wdpjhMRjoxiTXwz4xis+PEz0TW1YlAyeJgEi8SDLdFgzBM1YyGYH2hndM/xPEqCX8IRkhikZW02gKjPkzHTMcjE5iXlNMyId8ZEMlvnaMBkYyhJIk4MT5ndE0zV+dMkmTDNfGaS3JuMlmdTNfFEzZsnMhmRZIyhQ4bJzMl2KzMFm0yoZ9M/mBlKxkPo+Z0sgWSjLlloyFZ75TKbVLwCqzF+N7NmQXzPYK5a+nfR8SxCfZJj3mr7VMYPztwZjpqwHM/OeKtkt8Cx3uLrDPxLEoAyxIbTjpwjdk68SZ9Y5yESAlBXhM2lsxaEQUnHCE9pWHBplSRpJXj68AEOGUTHbyZyiAweQGIOWznBznsOEYnoXKAn4pmIvQD/DnOgBYhchksMuReLRxzBxyjcliFl2cgNzTxwcslEoy7kZzg50Aa0tBWHw5z4YdpCAtcTUkIsc5xcygMT1Yn5TPkdQCgPQGGiUA6J+RGwOHOJCyS55pxcRGZDRKyBZE68JJv1CsTgD9UO5JAPAHVF0IEe7I2UvKSqL/gY5mbTwYQIAGVz44CFQnjnObmC0YJJM/saiV1bBzM2paIBU91vjP8hB86VHtG0Gn6TgMK8teaag3nM9Ty006EcqO9EMCxCRElaYvEPHYN785keCDcAmx4ACAWAAIJsJbntAUQM84ORXKoW9cOxv6HRhMWwl/BkIJAgiA/JgFqF754SO+QbVREDygJHcusNXJ7kEIXocioCUPMiAjy0CY8xSpPKZhpy2UBeEpgQTiCyCxpOig4dAMqA4hGAr/RADKLYCnYLpfQCkcvDurRTVII0aRKAOIWYDTEe1eJgEAOBeoKgvINlrcE6kYt4JiErCIm3ARdpVg86BWqdgYVNlAkb8iBZvxbmcKuSWIXCUwDoIL5UJe4n0QeK0LK8yx+ABdINLqSGLSeb0QPBNJ262L4xM0tjCZJbmtYPhbI1DBawQrhAqA/OVKfzitY2tn+bgmiXvGZ5ySClMpNjpwSQzBzc2lTV3m/PekW1Pp3vUpLbUfIB9iZxUrporKkXwypZhsvjDHKFkAyRZeynOXnPyAVpDlFsuZZrO2XGZMcs8kueIgNl3KgJpy9GRctYU/z3lSMk5Q8uFk7Lnlg8uueMH+VL9VQgK9mfLPOXvkAFpnccrcoBX3LYVWs+FfhW7nSLvKksFFVCphUF84VIKx0TnN7mKL8V1yQlcs0eVQdSVg84eZCqpVoqiVGKklRjI0UTy3uTK45SysjHntoxUK96oSFeY2yX2/fe2cQUdnD9l6O0DZmIUfRT9421TN/GUWRYzkOugrI/Lyz9zr976eIwRSvGFq5RtVrEr4EyGQkiKYIH3f8K2MurnBC6bGW6bJPpiDKFRz/PllEEzYbNdeLrHjgnNZhYRpEYtDWgdNqAby+h8IYFtbVCF0t4W22UdPMpemGi3exolZV7zNHW0fpmytpmjI2aOiNmAAKk4iGomEXffQmdAADcPK72MKqWZXMBVVMqFT0JwjWyAUyYkavHDTFD8v2sqpXtmJbWUB8xU/b2cWJ/QCTVB/sy/IpEAGGpBRvQzEZLzWjbzyIkchcXLzvrtok5YajkIFDTkYx51PkdvIesoC1yWsEKpwieooCALxyhPK9TermAcLwMx3bYdIWXnVT15AQcZUhJXg5N10+RHBHWBMiyJYYz2AVGsFnCHjkJOIZABSOfzzEX+9QaxbKJFEDAEJQ5EfCLmexxA4gA3UEuFBB6FV2a/w59IHHQ2fZKgLcigD9RyJYacNeGz7DkBMEgrH+PU41VeonGVhdwVywctNC/V1hNpSKtjKmh4AYYmIVreaSMOfWq5+lRkkoNfUiCDwLeoG8DQgGUBexDQd6nwZQAfWfosy6JaQgN0IjGo8Kl8uvHTQ6Vg5AmJSC3oQtumDq+ASQT+VcMoVBMaNpiNaNMP5CkwJ14DRWoOJcDNDZxA01jZUqMV7Y9+S1PUHUTPhWL6l0gJxUKBcUTASyxiqKXay0m9o+pVimxbwSzjnUyp8gKaYoXvVCa8pVJa8bWhym8hTFhvVYIXE1AMCV1EczEYHlGCKg+gSgSJLkJVroLnIfq4NEmoNHYLsktTEthmvLaflfp1bNlU8oabvkr1Nax0NpuEFAqzlIK+bVioAhXqz19clkZSr4wOavlmIzbQepW26altR22mabM8xZkXYrpKFVmVVCyhD4ooN0KqHIC4Bfu8ncoKBETHtrbZEqrtQ7MqwyrMxabcLT0yiJiBxQcJEIPimHbjodhXsstGOqK7cL8gpBJlFE3GqItMUp2AWlQDEBbo4dZ8YdiWu4iohvkG/GQdUs0jdSUFaaUVJ+iQksgnAPgU1uTptWoD3k3jLoFuGS3/hg1gtSoIRDx26hEddNfSBrUCVbNd4brPQd1UPEBBbErdbHRtUpYvhky8JazpLwAg9tlWH3JIOc2Z0rt7oUEoiFBKUAyZad4wq+P+H43K0ZeLELeGG30w8zUI9gfxHitCVT8PG63QmRQX5w0BnIhER3VkoerjhF4DOd3VpO4Be6Rc+20ic8EtaCakIKkMcKcHcm9Cq42u+HZLo10K7bxTO32B7vj3e6k9qkVAXnrJ0LMHIgIp3UYzXaOVpoZC+xBgAFIrAqF5UVjo1vyYO7pAc+LjXLtvopTL0Mjf6o5Khyn5Y9bkhPbIszKLVpoDgmPAIV6xhATQUgCDSPvWyYSkEQIhSBazX2w1ui4uoVpmTuyspC9Q2y8m9LG0FtTRk2jZVaNzUxc6dcbDGVUrt0i1oduAWHSmWHTDskg1mKvr7B+y5B+eden7Mzju2qgHt1yJ7S9pTjvbPt32x0KrGu3uYzZl7NxlCveTCcN8oqgHeKpTHA6pVoO3tWBLPhVYixSKfhYR0iEIVH0mqhiTjlkDCRDsFOoiMRFwPnMCDNkJZJ6t0jFlRpbdEyNkIRb6YpkO00XVP2BKt7zBS5FekQNpK1J5A4bLRujtUF6N9F48ONuykUII669HccsMTvX0SlyoawGMPMJOBIIXA4h7TIxlPjYz9ZBS30X7t6jRqFD7wxSDtBUNyJPBOcDRty3ipMQyopmvQzjrF1GHuW7BpARWCC23B/d/UVDCrwVG9SkM1AT4dYl8VTRJsK8QXTenMFRQk4gZSViKGLQZNWCcSd1gIRv0m1U1yyz3ptu+lTac11SQMWwnrbDsAA/FwBiPy6fskAAAD5WHfAWmb0b6IAhSHDluBker7H4Mb1lmnRoI46POZ9HIDsR6A7TEmOnwMpThyoC4YpkIda+cxqzAsYHYXNtELMK8By0xwMHIArtTY/LsgCl0uDZ0Hg2snmYhGfMlxqtQbLOObILjhB09lgfb6NVuMB7Y9jZh2VXIJMrk1CG8gr1tqisgO0g9bhB2ftoUsKUdJGvW7Gr8u3PJ9f+V/Gz7PdFep9fMLW4pGJ9m0uLVNJYOARCTwCkFXIfDIkS+OMQLnvREyBPo5gYAZbPACcAxBhIWELHNybAC8nig/JwU04HpDmZrlPUkUzEyVpVk+Yxq3XuoHpTiErFWhl+DhoSNS8BaZm2RMsF6WIBi0z/BYLhDCBRAOtfAP6LLt+7wc1o3J6aO0MoCIhegIhFEt0ThF/NqmF4hEJyUniqQHBi8bcgANM6bkVRSqW6SBKd55tb9jR+/U/kf3mjs1L+gjAqgWWvSUzHvcbS0fWV+88M4a7oqcCUAJgbtPdS9gcz3ZtUoTvQaDFPTFV990T89cg1iehg4nh+tB0XTtFYQKJ3EbvPHay1MNhBzeREVsST2/23T5+fdXdg82OabJAohMv8h7ipMbwaTDhr7VhAHO1JboUVeIksklGI6zIKJJfSqsKKAVCevpJZIObqRqDxWPU0Up/Ej07RC9gEewBeAFRWApQXqWLFKCvCswAA0iAz1NfovIHO5qY1ElBShkAQOphYBC5SkFhB+CpXTsXnYTlBWkuh84PFhDPnJWk+aljarWhpQ7hlRaYz+b/MAXBmYTEZuBa4XaGaWC+eloagAxfS/xoTYZhEy8IdIYmyRh6fzXlFYK35WR71rkaYFr78jziw/eTwDbFHklAtegcQLvOnY1sSmyo0jxwlN6Y8q52RMdFZAHkRD/S9KRxaDYKr7ByOoKPIa+Bt6Mp3F8JqMy4NVwNAHsI9ufqEPXxZIP0O1n1lRC3A2BfppVFZNx6eb9LCkQy+unqNVN8zxbB/asszWtHn9/vXNbcbvSOiHjTxuizxeAavH3jblzdt8Yjb/HDldZpc42dXP1qOZwetZmsej7fnfz/5wC8BbAuQAAAvJeH4tG6njUQUUCsAjoRZXj0Iedh1b0BQQ0AqFjQOhc0CYXhk1asq4uZPqPMVzzZ64zVerTNKMZ/TRqzRdiw5WnL9gDq5AH6QDIDrDFt0Ate3bjNq6y15cx7CquXMWYKxupI6LvNcA4gGgTsiSi4gG7xESQJ452Sr7UXmrQF0C/YHPNTx7APUn2tCFLortFrt1qZvdabNrn1rUAF687T2UNsoIX1sdj9ZYB/XOkgNsdsDaau0XHLDFyG+ImhvDBYbrxhG9ddr7lW7rlVta/yuwML9ATxV8jKslqvNiWzxBts52oxOdml64O4WfzbUa7dgjEbJHQFE6yo7/c8LfkAOsarc2+Dr9VRlcewvbZVmwm9XQYfuBOJidRXCNagJNthAoFO+TJDpwCPZxWmGMa7FJx9IqAYk2F0/M7Y2EBSnkikM+ZvHBgaX98R8L4JqMugsAbb8Gggu/EMZWsxgDkUhoVKtTeczI0+SjnQ0wmhc0AmEyLphNi6YSEunJFLnILZS4BoQVCIRnvnEbo7/0CLbAe7YCPncTx2oeMWAFojmhDCDJqvbLo0sxXFlo2gswlYm0Zm2jWZja503rbR9z9TNnA18ZvbDs+bm1sOPWurMXsubc95DgvbrWC3UTJBkWx2dtwUHsTGrNiThr7PuI7b8qpg4FLn4rINbW9tVeufzyHjk5JUPwDmpcuCt9bf5IQ3NwRaXGdVnjTwI8SPiTnPNelNiQptH006cRleChfknLQrpthwi2DY6pI3a06CnrIBygKMg0F74yjUKyYTWj6ahLLUOy3ca1u+2teG+SBxWAFKkWEW29pZBucTO5mU1Sy1MyYSLO+9LRqV/6RNvTlFqCri9zpogFKsz3177seexCx8xMOaVaVA6RjCEc2qy1fxq65TNOMb2s+D963SvbBM1nJH6yTezI5diXGiDu94W5827XSrKDzsgdsd36XxbP6bB+WzB3Psumt1DOO+1o55vrZFjOtmJvY/ynJkga9wGBjLxq5UBSo3tpnpxzDs77Dzi5XBjHbfNEJnOkCROx5wobedqGPnYBIgnobAJGGUXM6LISwRsNcEnDRLsAmIRPEyEP8a6QI2oSFdTsYjArkwkjQGq42Rq58Xvzq2FN/bF88J4UAILLpWBY7Xp3+Jcf4tOgmWmO2UMXxdbo8bqm6Duk4MboEgr10xBeSGmHQg79Cn20kpDisPk1I288oPbTOJWn9JZv6XaPsdAzpnEjzjPfZMf+O9HXdTm8858fwRFgZ6He+bjRP72vmNj6FM7KvtIhFV1lj/SFFzI/P2A6q/PJR2pMPTLhDTabM8A/sMPT8blzy7qoFYJrJqb9oUHd3m5laiHOFkx4A/5bTQLVKD9kLADmdEQ4Xfz5h4i75ABAOgaR1Z7pcjX8tvVZbeUwNtaZ928zHDi51w7LYj2Ur9tO0fmoxnKPSYZaltuo5OOz2pHyHdEiy45srhWwEMdMJmGzB2y5NRYYcP4DQBF9u1HQWsHODUALhmwy4AwLq8HDqAACiAWLAEF3KWRMC1aIDEmEdcphIA6QCLHEB2ACBMgiIWgOkFSCpA4gmQN0jG9SCo0GAWwVGgIHSDpA4gAgZN+jTRpbAm4DATIGgGDfgwWwAb2gJkCyBxBUaJAHYDsDdJKA0A6NNAOkCbjRu0AbpNGqjWDdNw83kb80HEHRoCA4gbpIt0uEMC6um4Nb9ICQEyBVuGAQ701KjTiCpBm3EWVGukCzdLuIsOwTILQBDcJAGA5bqt3u7Hf+uIAjATIGG/NDpBaAobqt0u7dICB0aOwCLJkAixukR3OwJQLO/RqpABAewSoH+9Rq8nS3576t2gDjdbBl3qQBgHsHRrJuBAr71GqkBICo1m36QR9zsHSDtv0g6NDdzG6bhNwdgp73V6kAiwtvsaGNAQIm+Dcoe4gr71txUAiy0ABAwHnN0G47eDv0a6NLYO+5I8Bvf3mQVIF+7Xe0AGA6QVGk3DDdZAoPVHnd0+8k9duSAWwRIFsDdJ41lP/H896h7iBNxsP6NLlGgEnchQ3S6b+0CQDdI7AGAuNQjxFgEC0A3SXKKNy2+renuz3UAZ17gFdfuvqSe5TAmmDHdAA=== -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=hypha-dao/hypha-web&utm_content=717):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-10T12:40:34Z
body: 
<!--- [schema diff GitHub action comment identifier] -->
<!--- [diff digest: bf5ef50301d8961eccbbc5517c56e7d22263d9e384a38b2c3ddf7b1c847fbfd9] -->

# <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/neondatabase/schema-diff-action/refs/heads/main/docs/logos/logo-dark.svg"><img alt="Neon logo" src="https://raw.githubusercontent.com/neondatabase/schema-diff-action/refs/heads/main/docs/logos/logo-light.svg" width="24" height="24"></picture> Neon Schema Diff summary

Schema diff between the compare branch ([preview/pr-717-feat/694-plitzenberger-Tags-in-Network-view](https://console.neon.tech/app/projects/bold-fire-40727812/branches/br-lucky-frost-a21t91iv)) and the base branch ([main](https://console.neon.tech/app/projects/bold-fire-40727812/branches/br-orange-sound-a2fftiot)).

- Base branch: main ([br-orange-sound-a2fftiot](https://console.neon.tech/app/projects/bold-fire-40727812/branches/br-orange-sound-a2fftiot)) 🔒
- Compare branch: preview/pr-717-feat/694-plitzenberger-Tags-in-Network-view ([br-lucky-frost-a21t91iv](https://console.neon.tech/app/projects/bold-fire-40727812/branches/br-lucky-frost-a21t91iv)) 
- Database: neondb

```diff
Index: neondb-schema.sql
===================================================================
--- neondb-schema.sql	Branch main
+++ neondb-schema.sql	Branch preview/pr-717-feat/694-plitzenberger-Tags-in-Network-view
@@ -330,9 +330,10 @@
     slug text NOT NULL,
     created_at timestamp without time zone DEFAULT now() NOT NULL,
     updated_at timestamp without time zone DEFAULT now() NOT NULL,
     web3_space_id integer,
-    links jsonb DEFAULT '[]'::jsonb
+    links jsonb DEFAULT '[]'::jsonb,
+    categories jsonb DEFAULT '[]'::jsonb
 );
 
 
 ALTER TABLE public.spaces OWNER TO neondb_owner;

```

This comment was last updated at Thu, 10 Apr 2025 12:40:33 GMT

author_association: NONE