Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ the /landing package contains the marketing and landing site.

The web package is copied into the landing package during the build process for deployment and all deployed as a single site on a single worker.

Do not worry about migrations either client side or backend unless specifically instructed to do so in the prompt. This project is not in production and has no users.

## Coding Standards

- Do not use emojis in code, comments, documentation, or commit messages.
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
"description": "Shared UI components for Corates built with Zag.js and SolidJS",
"type": "module",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"exports": {
".": "./src/index.js",
".": {
"types": "./src/index.d.ts",
"default": "./src/index.js"
},
"./zag": "./src/zag/index.js",
"./zag/*": "./src/zag/*.jsx"
},
Expand Down
142 changes: 142 additions & 0 deletions packages/ui/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Component, JSX } from 'solid-js';

// Editable
export interface EditableProps {
value?: string;
onSubmit?: (_value: string) => void;
activationMode?: 'focus' | 'dblclick' | 'click' | 'none';
variant?: 'default' | 'heading';
showEditIcon?: boolean;
readOnly?: boolean;
class?: string;
placeholder?: string;
}
export const Editable: Component<EditableProps>;

// Collapsible
export interface CollapsibleProps {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (_open: boolean) => void;
disabled?: boolean;
trigger?: (_api: { open: boolean }) => JSX.Element;
children?: JSX.Element;
}
export const Collapsible: Component<CollapsibleProps>;

// Menu
export interface MenuItem {
value: string;
label: string;
icon?: JSX.Element;
destructive?: boolean;
separator?: boolean;
}
export interface MenuProps {
trigger: JSX.Element;
items: MenuItem[];
onSelect?: (_details: { value: string }) => void;
placement?: string;
hideIndicator?: boolean;
}
export const Menu: Component<MenuProps>;

// Dialog
export interface DialogProps {
open?: boolean;
onOpenChange?: (_open: boolean) => void;
title?: string;
description?: string;
children?: JSX.Element;
}
export const Dialog: Component<DialogProps>;

export interface ConfirmDialogProps {
open?: boolean;
onOpenChange?: (_open: boolean) => void;
title?: string;
description?: string;
confirmLabel?: string;
cancelLabel?: string;
onConfirm?: () => void;
onCancel?: () => void;
destructive?: boolean;
}
export const ConfirmDialog: Component<ConfirmDialogProps>;
export function useConfirmDialog(): {
open: () => boolean;
setOpen: (_open: boolean) => void;
confirm: () => Promise<boolean>;
};
Comment on lines +66 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix incomplete and incorrect return type for useConfirmDialog.

The declared return type doesn't match the actual implementation shown in Dialog.jsx. The actual hook returns additional properties and has different signatures:

Based on the implementation in Dialog.jsx (lines 242-320):

  • isOpen: () => boolean (not open: () => boolean)
  • open: (dialogConfig) => Promise<boolean> (takes config, returns Promise, not a getter)
  • close: () => void (missing)
  • setLoading: (loading: boolean) => void (missing)
  • ConfirmDialogComponent: () => JSX.Element (missing)
  • dialogProps: () => {...} (missing)
Apply this diff to correct the return type:
-export function useConfirmDialog(): {
-  open: () => boolean;
-  setOpen: (_open: boolean) => void;
-  confirm: () => Promise<boolean>;
-};
+export function useConfirmDialog(): {
+  isOpen: () => boolean;
+  open: (_config: {
+    title?: string;
+    description?: string;
+    confirmText?: string;
+    cancelText?: string;
+    variant?: 'default' | 'destructive';
+  }) => Promise<boolean>;
+  close: () => void;
+  setLoading: (_loading: boolean) => void;
+  ConfirmDialogComponent: () => JSX.Element;
+  dialogProps: () => {
+    open: boolean;
+    onOpenChange: (_newOpen: boolean) => void;
+    onConfirm: () => void;
+    loading: boolean;
+    title?: string;
+    description?: string;
+    confirmLabel?: string;
+    cancelLabel?: string;
+    destructive?: boolean;
+  };
+};
🤖 Prompt for AI Agents
In packages/ui/src/index.d.ts around lines 66 to 70, the declared return type
for useConfirmDialog is incomplete and incorrect compared to Dialog.jsx; update
the type to match the implementation: replace the current shape with an object
exposing isOpen(): boolean, open(dialogConfig): Promise<boolean>, close(): void,
setLoading(loading: boolean): void, ConfirmDialogComponent(): JSX.Element, and
dialogProps(): { title?: string; description?: string; confirmText?: string;
cancelText?: string; hideCancel?: boolean; loading?: boolean; intent?: string; }
(adjust property names/types to match Dialog.jsx), ensuring imports/types (e.g.,
JSX.Element) are available in the d.ts file.


// Select
export interface SelectOption {
value: string;
label: string;
}
export interface SelectProps {
options: SelectOption[];
value?: string;
onChange?: (_value: string) => void;
placeholder?: string;
disabled?: boolean;
}
export const Select: Component<SelectProps>;

// Tooltip
export interface TooltipProps {
content: string | JSX.Element;
children: JSX.Element;
placement?: string;
}
export const Tooltip: Component<TooltipProps>;

// Toast
export interface ToastOptions {
title?: string;
description?: string;
type?: 'info' | 'success' | 'warning' | 'error';
duration?: number;
}
export const Toaster: Component;
export const toaster: {
create: (_options: ToastOptions) => void;
success: (_options: ToastOptions) => void;
error: (_options: ToastOptions) => void;
warning: (_options: ToastOptions) => void;
info: (_options: ToastOptions) => void;
};
export function showToast(_options: ToastOptions): void;

// Primitives
export function useWindowDrag(_options?: { onDragEnd?: () => void }): {
isDragging: () => boolean;
};

// Utilities
export function cn(..._classes: (string | undefined | null | false)[]): string;

// Re-export other components (add types as needed)
export const Accordion: Component<any>;
export const Avatar: Component<any>;
export const Checkbox: Component<any>;
export const Clipboard: Component<any>;
export const CopyButton: Component<any>;
export const Combobox: Component<any>;
export const FileUpload: Component<any>;
export const FloatingPanel: Component<any>;
export const NumberInput: Component<any>;
export const PasswordInput: Component<any>;
export const PinInput: Component<any>;
export const Popover: Component<any>;
export const Progress: Component<any>;
export const QRCode: Component<any>;
export const RadioGroup: Component<any>;
export const Splitter: Component<any>;
export const Switch: Component<any>;
export const Tabs: Component<any>;
export const TagsInput: Component<any>;
export const ToggleGroup: Component<any>;
export const Tour: Component<any>;
export const TourProvider: Component<any>;
export function useTour(): any;
10 changes: 6 additions & 4 deletions packages/ui/src/zag/Accordion.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as accordion from '@zag-js/accordion';
import { normalizeProps, useMachine } from '@zag-js/solid';
import { createMemo, createUniqueId, For, splitProps, mergeProps } from 'solid-js';
import { createMemo, createUniqueId, For, splitProps } from 'solid-js';

/**
* Accordion - Vertically stacked expandable sections
Expand All @@ -19,9 +19,11 @@ import { createMemo, createUniqueId, For, splitProps, mergeProps } from 'solid-j
export function Accordion(props) {
const [local, machineProps] = splitProps(props, ['items', 'class']);

const context = mergeProps(machineProps, { id: createUniqueId(), collapsible: true });

const service = useMachine(accordion.machine, context);
const service = useMachine(accordion.machine, () => ({
id: createUniqueId(),
collapsible: true,
...machineProps,
}));
Comment on lines +22 to +26
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Spread order fixed; consider moving ID generation outside the factory.

The default prop override issue from the previous review has been resolved—collapsible and other defaults now correctly appear before ...machineProps, allowing user configuration to take precedence.

However, following the pattern demonstrated in packages/ui/src/zag/Editable.jsx (lines 105-127), calling createUniqueId() inside the factory function should be avoided. Generate the ID once in the component body to ensure stability.

🔎 Apply this pattern for consistency:
+  const id = createUniqueId();
+
   const service = useMachine(accordion.machine, () => ({
-    id: createUniqueId(),
+    id,
     collapsible: true,
     ...machineProps,
   }));
📝 Committable suggestion

‼️ 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.

Suggested change
const service = useMachine(accordion.machine, () => ({
id: createUniqueId(),
collapsible: true,
...machineProps,
}));
const id = createUniqueId();
const service = useMachine(accordion.machine, () => ({
id,
collapsible: true,
...machineProps,
}));
🤖 Prompt for AI Agents
packages/ui/src/zag/Accordion.jsx around lines 22 to 26: currently
createUniqueId() is called inside the useMachine factory; move ID generation out
of the factory so the ID is created once per component render (e.g., generate
the ID in the component body before calling useMachine) and then pass that
stable id into the factory parameters, ensuring the id does not get re-generated
on each factory invocation.


const api = createMemo(() => accordion.connect(service, normalizeProps));

Expand Down
16 changes: 7 additions & 9 deletions packages/ui/src/zag/Clipboard.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as clipboard from '@zag-js/clipboard';
import { normalizeProps, useMachine } from '@zag-js/solid';
import { createMemo, createUniqueId, Show, splitProps, mergeProps } from 'solid-js';
import { createMemo, createUniqueId, Show, splitProps } from 'solid-js';
import { FiCopy, FiCheck } from 'solid-icons/fi';

/**
Expand All @@ -20,12 +20,11 @@ import { FiCopy, FiCheck } from 'solid-icons/fi';
export function Clipboard(props) {
const [local, machineProps] = splitProps(props, ['label', 'showInput', 'children', 'class']);

const context = mergeProps(machineProps, {
const service = useMachine(clipboard.machine, () => ({
id: createUniqueId(),
timeout: 3000,
});

const service = useMachine(clipboard.machine, context);
...machineProps,
}));
Comment on lines +23 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Spread order fixed in both components; consider moving ID generation outside the factory.

The default prop override issues from the previous review have been resolved in both the Clipboard and CopyButton components—timeout defaults now correctly precede ...machineProps, allowing user-provided timeout values to override the default.

However, both components call createUniqueId() inside the factory function. Following the pattern in packages/ui/src/zag/Editable.jsx (lines 105-127), generate the ID once in each component body to ensure stability and consistency with best practices.

🔎 Apply this pattern for both components:

For Clipboard component (lines 23-27):

+  const id = createUniqueId();
+
   const service = useMachine(clipboard.machine, () => ({
-    id: createUniqueId(),
+    id,
     timeout: 3000,
     ...machineProps,
   }));

For CopyButton component (lines 90-94):

+  const id = createUniqueId();
+
   const service = useMachine(clipboard.machine, () => ({
-    id: createUniqueId(),
+    id,
     timeout: 3000,
     ...machineProps,
   }));

Also applies to: 90-94

🤖 Prompt for AI Agents
In packages/ui/src/zag/Clipboard.jsx around lines 23-27 and
packages/ui/src/zag/CopyButton.jsx around lines 90-94, move the call to
createUniqueId() out of the useMachine factory so the ID is generated once per
component render (like Editable.jsx lines 105-127); create a const id =
createUniqueId() at the top of the component body and then pass that id into the
useMachine factory return object instead of calling createUniqueId() inside the
factory, ensuring a stable, single ID instance while keeping timeout and
...machineProps order as is.


const api = createMemo(() => clipboard.connect(service, normalizeProps));

Expand Down Expand Up @@ -88,12 +87,11 @@ export function CopyButton(props) {
'class',
]);

const context = mergeProps(machineProps, {
const service = useMachine(clipboard.machine, () => ({
id: createUniqueId(),
timeout: 3000,
});

const service = useMachine(clipboard.machine, context);
...machineProps,
}));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const api = createMemo(() => clipboard.connect(service, normalizeProps));

Expand Down
17 changes: 4 additions & 13 deletions packages/ui/src/zag/Combobox.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import * as combobox from '@zag-js/combobox';
import { Portal } from 'solid-js/web';
import { normalizeProps, useMachine } from '@zag-js/solid';
import {
createMemo,
createSignal,
createUniqueId,
For,
Show,
splitProps,
mergeProps,
} from 'solid-js';
import { createMemo, createSignal, createUniqueId, For, Show, splitProps } from 'solid-js';
import { FiChevronDown, FiX, FiCheck } from 'solid-icons/fi';

/**
Expand Down Expand Up @@ -57,9 +49,10 @@ export function Combobox(props) {
}),
);

const context = mergeProps(machineProps, {
const service = useMachine(combobox.machine, () => ({
id: createUniqueId(),
openOnClick: true,
...machineProps,
get collection() {
return collection();
},
Expand All @@ -73,9 +66,7 @@ export function Combobox(props) {
);
setOptions(filtered.length > 0 ? filtered : items);
},
});

const service = useMachine(combobox.machine, context);
}));

const api = createMemo(() => combobox.connect(service, normalizeProps));

Expand Down
17 changes: 13 additions & 4 deletions packages/ui/src/zag/Editable.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as editable from '@zag-js/editable';
import { normalizeProps, useMachine } from '@zag-js/solid';
import { createMemo, createUniqueId, Show, mergeProps } from 'solid-js';
import { createMemo, createUniqueId, Show, mergeProps, createEffect, on } from 'solid-js';
import { FiCheck, FiX, FiEdit2 } from 'solid-icons/fi';
import { cn } from '../lib/cn.js';

Expand Down Expand Up @@ -105,9 +105,8 @@ export default function Editable(props) {

const service = useMachine(editable.machine, () => ({
id,
// Use defaultValue for initial value - Zag manages internal editing state
// The value prop is used to sync from parent when it changes externally
defaultValue: value() ?? defaultValue(),
// Always use defaultValue - Zag manages internal editing state
defaultValue: value() ?? defaultValue() ?? '',
placeholder: placeholder(),
disabled: disabled(),
readOnly: readOnly(),
Expand All @@ -129,6 +128,16 @@ export default function Editable(props) {

const api = createMemo(() => editable.connect(service, normalizeProps));

// Sync external value changes ONLY when not editing
// This handles cases like the parent updating value after a successful save
createEffect(
on(value, newValue => {
if (newValue !== undefined && newValue !== api().value && !api().editing) {
api().setValue(newValue);
}
}),
);

return (
<div {...api().getRootProps()} class={cn('group inline-block', classValue())}>
<Show when={label()}>
Expand Down
54 changes: 33 additions & 21 deletions packages/ui/src/zag/Menu.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as menu from '@zag-js/menu';
import { Portal } from 'solid-js/web';
import { normalizeProps, useMachine } from '@zag-js/solid';
import { createMemo, createUniqueId, Show, For, splitProps, mergeProps } from 'solid-js';
import { createMemo, createUniqueId, Show, For, splitProps } from 'solid-js';

/**
* Menu - Dropdown menu for actions
Expand All @@ -16,6 +16,7 @@ import { createMemo, createUniqueId, Show, For, splitProps, mergeProps } from 's
* - placement: Placement - Menu placement (default: 'bottom-start')
* - closeOnSelect: boolean - Close menu on selection (default: true)
* - inDialog: boolean - Set to true when used inside a Dialog
* - hideIndicator: boolean - Hide the dropdown indicator chevron
* - class: string - Additional class for content
*
* MenuItem:
Expand All @@ -28,15 +29,21 @@ import { createMemo, createUniqueId, Show, For, splitProps, mergeProps } from 's
* - groupLabel?: string - Render as group label
*/
export function Menu(props) {
const [local, machineProps] = splitProps(props, ['trigger', 'items', 'inDialog', 'class']);
const [local, machineProps] = splitProps(props, [
'trigger',
'items',
'inDialog',
'hideIndicator',
'placement',
'class',
]);

const context = mergeProps(machineProps, {
const service = useMachine(menu.machine, () => ({
id: createUniqueId(),
closeOnSelect: true,
positioning: { placement: 'bottom-start' },
});

const service = useMachine(menu.machine, context);
positioning: { placement: local.placement || 'bottom-start' },
...machineProps,
}));
Comment on lines +32 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

New props implemented correctly; consider moving ID generation outside the factory.

The new hideIndicator and placement props are properly extracted and used:

  • placement is correctly converted to the machine's positioning configuration (line 44)
  • hideIndicator is appropriately used to conditionally render the indicator (line 102)
  • Default values are placed before ...machineProps, allowing user overrides

However, consistent with the pattern in packages/ui/src/zag/Editable.jsx (lines 105-127), createUniqueId() should be called once in the component body rather than inside the factory function to ensure stability.

🔎 Apply this pattern:
+  const id = createUniqueId();
+
   const service = useMachine(menu.machine, () => ({
-    id: createUniqueId(),
+    id,
     closeOnSelect: true,
     positioning: { placement: local.placement || 'bottom-start' },
     ...machineProps,
   }));
📝 Committable suggestion

‼️ 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.

Suggested change
const [local, machineProps] = splitProps(props, [
'trigger',
'items',
'inDialog',
'hideIndicator',
'placement',
'class',
]);
const context = mergeProps(machineProps, {
const service = useMachine(menu.machine, () => ({
id: createUniqueId(),
closeOnSelect: true,
positioning: { placement: 'bottom-start' },
});
const service = useMachine(menu.machine, context);
positioning: { placement: local.placement || 'bottom-start' },
...machineProps,
}));
const [local, machineProps] = splitProps(props, [
'trigger',
'items',
'inDialog',
'hideIndicator',
'placement',
'class',
]);
const id = createUniqueId();
const service = useMachine(menu.machine, () => ({
id,
closeOnSelect: true,
positioning: { placement: local.placement || 'bottom-start' },
...machineProps,
}));
🤖 Prompt for AI Agents
In packages/ui/src/zag/Menu.jsx around lines 32 to 46, createUniqueId() is
currently invoked inside the useMachine factory which can create a new id each
time the factory runs; move the call to createUniqueId() to the top of the
component body (before useMachine) to generate the id once per component
instance, then pass that id into the factory via the id property (keep
closeOnSelect, default positioning and spread ...machineProps as-is).


const api = createMemo(() => menu.connect(service, normalizeProps));

Expand Down Expand Up @@ -87,21 +94,26 @@ export function Menu(props) {

return (
<>
<button {...api().getTriggerProps()} class='inline-flex items-center gap-1'>
<button
{...api().getTriggerProps()}
class='inline-flex items-center gap-1 p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors'
>
{local.trigger}
<span
{...api().getIndicatorProps()}
class='transition-transform data-[state=open]:rotate-180'
>
<svg class='w-4 h-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
d='M19 9l-7 7-7-7'
/>
</svg>
</span>
<Show when={!local.hideIndicator}>
<span
{...api().getIndicatorProps()}
class='transition-transform data-[state=open]:rotate-180'
>
<svg class='w-4 h-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
d='M19 9l-7 7-7-7'
/>
</svg>
</span>
</Show>
</button>
<Show when={api().open}>
<Show when={!local.inDialog} fallback={content()}>
Expand Down
Loading