public inbox for [email protected]
 help / color / mirror / Atom feed
From: Muhammad Rizki <[email protected]>
To: Ammar Faizi <[email protected]>
Cc: Muhammad Rizki <[email protected]>,
	Alviro Iskandar Setiawan <[email protected]>,
	GNU/Weeb Mailing List <[email protected]>
Subject: [PATCH v2 09/12] feat(ui): add dropdown-menu and update bits-ui version
Date: Sun,  9 Mar 2025 02:26:51 +0700	[thread overview]
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>

Added dropdown-menu UI component and updated bits-ui version.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 package-lock.json                             |  8 +--
 package.json                                  |  2 +-
 .../dropdown-menu-checkbox-item.svelte        | 40 +++++++++++++++
 .../dropdown-menu-content.svelte              | 27 ++++++++++
 .../dropdown-menu-group-heading.svelte        | 19 +++++++
 .../dropdown-menu/dropdown-menu-item.svelte   | 23 +++++++++
 .../dropdown-menu/dropdown-menu-label.svelte  | 23 +++++++++
 .../dropdown-menu-radio-item.svelte           | 30 +++++++++++
 .../dropdown-menu-separator.svelte            | 16 ++++++
 .../dropdown-menu-shortcut.svelte             | 20 ++++++++
 .../dropdown-menu-sub-content.svelte          | 19 +++++++
 .../dropdown-menu-sub-trigger.svelte          | 28 +++++++++++
 src/lib/components/ui/dropdown-menu/index.ts  | 50 +++++++++++++++++++
 13 files changed, 300 insertions(+), 5 deletions(-)
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
 create mode 100644 src/lib/components/ui/dropdown-menu/index.ts

diff --git a/package-lock.json b/package-lock.json
index 525092e..36dc7aa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,7 @@
         "@sveltejs/kit": "^2.0.0",
         "@sveltejs/vite-plugin-svelte": "^4.0.0",
         "autoprefixer": "^10.4.20",
-        "bits-ui": "^1.3.5",
+        "bits-ui": "^1.3.6",
         "clsx": "^2.1.1",
         "eslint": "^9.7.0",
         "eslint-config-prettier": "^9.1.0",
@@ -1898,9 +1898,9 @@
       }
     },
     "node_modules/bits-ui": {
-      "version": "1.3.5",
-      "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.5.tgz",
-      "integrity": "sha512-pfd8MK5Hp7bOvsW25LJrWVABmBIeAOH3g0pFPJLBIKlcqsaalEdBYejQmlSwrynEDX589aW3hTAWbhmWqRjjrQ==",
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.6.tgz",
+      "integrity": "sha512-0Ee7Ox5KqIBdio/+TG387Xlj6QJ0S7tHVS2K4DYiBHxBRbm6ni13i/pOoNDjeME6NOGUZxbSe4dNZIW3pGlxZA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
diff --git a/package.json b/package.json
index 52b0e4c..4d084e5 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "@sveltejs/kit": "^2.0.0",
     "@sveltejs/vite-plugin-svelte": "^4.0.0",
     "autoprefixer": "^10.4.20",
-    "bits-ui": "^1.3.5",
+    "bits-ui": "^1.3.6",
     "clsx": "^2.1.1",
     "eslint": "^9.7.0",
     "eslint-config-prettier": "^9.1.0",
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
new file mode 100644
index 0000000..660e574
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
@@ -0,0 +1,40 @@
+<script lang="ts">
+  import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
+  import Check from "lucide-svelte/icons/check";
+  import Minus from "lucide-svelte/icons/minus";
+  import { cn } from "$utils";
+  import type { Snippet } from "svelte";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    children: childrenProp,
+    checked = $bindable(false),
+    indeterminate = $bindable(false),
+    ...restProps
+  }: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
+    children?: Snippet;
+  } = $props();
+</script>
+
+<DropdownMenuPrimitive.CheckboxItem
+  bind:ref
+  bind:checked
+  bind:indeterminate
+  class={cn(
+    "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
+    className
+  )}
+  {...restProps}
+>
+  {#snippet children({ checked, indeterminate })}
+    <span class="absolute left-2 flex size-3.5 items-center justify-center">
+      {#if indeterminate}
+        <Minus class="size-4" />
+      {:else}
+        <Check class={cn("size-4", !checked && "text-transparent")} />
+      {/if}
+    </span>
+    {@render childrenProp?.()}
+  {/snippet}
+</DropdownMenuPrimitive.CheckboxItem>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
new file mode 100644
index 0000000..aada59c
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+  import { cn } from "$utils";
+  import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    sideOffset = 4,
+    portalProps,
+    ...restProps
+  }: DropdownMenuPrimitive.ContentProps & {
+    portalProps?: DropdownMenuPrimitive.PortalProps;
+  } = $props();
+</script>
+
+<DropdownMenuPrimitive.Portal {...portalProps}>
+  <DropdownMenuPrimitive.Content
+    bind:ref
+    {sideOffset}
+    class={cn(
+      "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
+      className
+    )}
+    {...restProps}
+  />
+</DropdownMenuPrimitive.Portal>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
new file mode 100644
index 0000000..123869c
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+  import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    inset,
+    ...restProps
+  }: DropdownMenuPrimitive.GroupHeadingProps & {
+    inset?: boolean;
+  } = $props();
+</script>
+
+<DropdownMenuPrimitive.GroupHeading
+  bind:ref
+  class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
+  {...restProps}
+/>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
new file mode 100644
index 0000000..cb92909
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+  import { cn } from "$utils";
+  import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    inset,
+    ...restProps
+  }: DropdownMenuPrimitive.ItemProps & {
+    inset?: boolean;
+  } = $props();
+</script>
+
+<DropdownMenuPrimitive.Item
+  bind:ref
+  class={cn(
+    "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+    inset && "pl-8",
+    className
+  )}
+  {...restProps}
+/>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
new file mode 100644
index 0000000..4a0b460
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+  import { cn } from "$utils";
+  import { type WithElementRef } from "bits-ui";
+  import type { HTMLAttributes } from "svelte/elements";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    inset,
+    children,
+    ...restProps
+  }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
+    inset?: boolean;
+  } = $props();
+</script>
+
+<div
+  bind:this={ref}
+  class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
+  {...restProps}
+>
+  {@render children?.()}
+</div>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
new file mode 100644
index 0000000..676f467
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
@@ -0,0 +1,30 @@
+<script lang="ts">
+  import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
+  import Circle from "lucide-svelte/icons/circle";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    children: childrenProp,
+    ...restProps
+  }: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
+</script>
+
+<DropdownMenuPrimitive.RadioItem
+  bind:ref
+  class={cn(
+    "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
+    className
+  )}
+  {...restProps}
+>
+  {#snippet children({ checked })}
+    <span class="absolute left-2 flex size-3.5 items-center justify-center">
+      {#if checked}
+        <Circle class="size-2 fill-current" />
+      {/if}
+    </span>
+    {@render childrenProp?.({ checked })}
+  {/snippet}
+</DropdownMenuPrimitive.RadioItem>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
new file mode 100644
index 0000000..da3d7b0
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+  import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    ...restProps
+  }: DropdownMenuPrimitive.SeparatorProps = $props();
+</script>
+
+<DropdownMenuPrimitive.Separator
+  bind:ref
+  class={cn("-mx-1 my-1 h-px bg-muted", className)}
+  {...restProps}
+/>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
new file mode 100644
index 0000000..8de03e7
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
@@ -0,0 +1,20 @@
+<script lang="ts">
+  import type { HTMLAttributes } from "svelte/elements";
+  import { type WithElementRef } from "bits-ui";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    children,
+    ...restProps
+  }: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
+</script>
+
+<span
+  bind:this={ref}
+  class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
+  {...restProps}
+>
+  {@render children?.()}
+</span>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
new file mode 100644
index 0000000..d999904
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+  import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    ...restProps
+  }: DropdownMenuPrimitive.SubContentProps = $props();
+</script>
+
+<DropdownMenuPrimitive.SubContent
+  bind:ref
+  class={cn(
+    "z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
+    className
+  )}
+  {...restProps}
+/>
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
new file mode 100644
index 0000000..cd0a621
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
@@ -0,0 +1,28 @@
+<script lang="ts">
+  import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
+  import ChevronRight from "lucide-svelte/icons/chevron-right";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    inset,
+    children,
+    ...restProps
+  }: WithoutChild<DropdownMenuPrimitive.SubTriggerProps> & {
+    inset?: boolean;
+  } = $props();
+</script>
+
+<DropdownMenuPrimitive.SubTrigger
+  bind:ref
+  class={cn(
+    "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+    inset && "pl-8",
+    className
+  )}
+  {...restProps}
+>
+  {@render children?.()}
+  <ChevronRight class="ml-auto" />
+</DropdownMenuPrimitive.SubTrigger>
diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts
new file mode 100644
index 0000000..2ea0e8c
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/index.ts
@@ -0,0 +1,50 @@
+import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
+import Content from "./dropdown-menu-content.svelte";
+import GroupHeading from "./dropdown-menu-group-heading.svelte";
+import Item from "./dropdown-menu-item.svelte";
+import Label from "./dropdown-menu-label.svelte";
+import RadioItem from "./dropdown-menu-radio-item.svelte";
+import Separator from "./dropdown-menu-separator.svelte";
+import Shortcut from "./dropdown-menu-shortcut.svelte";
+import SubContent from "./dropdown-menu-sub-content.svelte";
+import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
+
+const Sub = DropdownMenuPrimitive.Sub;
+const Root = DropdownMenuPrimitive.Root;
+const Trigger = DropdownMenuPrimitive.Trigger;
+const Group = DropdownMenuPrimitive.Group;
+const RadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+export {
+  CheckboxItem,
+  Content,
+  Root as DropdownMenu,
+  CheckboxItem as DropdownMenuCheckboxItem,
+  Content as DropdownMenuContent,
+  Group as DropdownMenuGroup,
+  GroupHeading as DropdownMenuGroupHeading,
+  Item as DropdownMenuItem,
+  Label as DropdownMenuLabel,
+  RadioGroup as DropdownMenuRadioGroup,
+  RadioItem as DropdownMenuRadioItem,
+  Separator as DropdownMenuSeparator,
+  Shortcut as DropdownMenuShortcut,
+  Sub as DropdownMenuSub,
+  SubContent as DropdownMenuSubContent,
+  SubTrigger as DropdownMenuSubTrigger,
+  Trigger as DropdownMenuTrigger,
+  Group,
+  GroupHeading,
+  Item,
+  Label,
+  RadioGroup,
+  RadioItem,
+  Root,
+  Separator,
+  Shortcut,
+  Sub,
+  SubContent,
+  SubTrigger,
+  Trigger
+};
-- 
Muhammad Rizki


  parent reply	other threads:[~2025-03-08 19:27 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 01/12] fix(svelte): use relative false Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 02/12] fix(avatar): change avatarImg state to use from auth.user.photo state Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 03/12] chore(profile): add toUpperCase() on getShortName() Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 04/12] fix(profile): make social fields default to empty string Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 05/12] chore(toaster): change toast message position and use richColors Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 06/12] chore(profile): reset password value on success Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 07/12] fix(profile-avatar): add delete avatar method Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 08/12] chore(profile): add space for password confirmation form Muhammad Rizki
2025-03-08 19:26 ` Muhammad Rizki [this message]
2025-03-08 19:26 ` [PATCH v2 10/12] chore(sidebar-menu): change sidebar menu look Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 11/12] chore(seo): move seo from layout to /home page Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 12/12] fix(auth): fix auth guard when credentials is invalid Muhammad Rizki
2025-03-08 19:38 ` [PATCH v2 00/12] Fix Auth Guard, Move SEO Ammar Faizi
2025-03-08 19:51   ` Alviro Iskandar Setiawan

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    [email protected] \
    [email protected] \
    [email protected] \
    [email protected] \
    [email protected] \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox