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
57 changes: 57 additions & 0 deletions .claude/docs/framework_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,63 @@

This guide covers advanced patterns, utilities, and implementation details specific to each framework.

## Icon Imports

When using icons from Lucide in examples, follow these framework-specific import patterns:

### React Icon Imports

```tsx
import { XIcon } from 'lucide-react'

// Usage
<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
```

### Solid Icon Imports

```tsx
import { XIcon } from 'lucide-solid'

// Usage
<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
```

### Vue Icon Imports

```vue
<script setup lang="ts">
import { XIcon } from 'lucide-vue-next'
</script>

<template>
<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
</template>
```

### Svelte Icon Imports

```svelte
<script lang="ts">
import { XIcon } from 'lucide-svelte'
</script>

<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
```

**Icon Naming Convention:**
- Always import icons with the `Icon` suffix (e.g., `XIcon`, `CheckIcon`, `ChevronDownIcon`)
- Use the full icon name directly from Lucide (don't rename with `as`)
- This maintains consistency across all framework examples

## Ref Synchronization Strategies

Each framework handles refs differently, requiring specific synchronization patterns:
Expand Down
84 changes: 51 additions & 33 deletions .storybook/styles/tags-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 8px;
gap: 4px;
}

[data-scope='tags-input'][data-part='label'] {
Expand All @@ -15,21 +15,27 @@
border: 1px solid #ccc;
width: 40em;
border-radius: 2px;
position: relative;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}

[data-scope='tags-input'][data-part='control'][data-disabled] {
background: #f9f9f9;
}
&[data-disabled] {
background: #f9f9f9;
}

[data-scope='tags-input'][data-part='control'][data-focus],
[data-scope='tags-input'][data-part='control']:focus {
outline: 2px solid rgb(234, 234, 234);
&[data-focus],
&:focus {
outline: 2px solid var(--ring-color);
outline-offset: 2px;
}

svg {
width: 1em;
height: 1em;
}
}

[data-scope='tags-input'][data-part='item'] {
display: inline-flex;
align-items: center;
display: inline-block;
}

[data-scope='tags-input'][data-part='item-preview'] {
Expand All @@ -44,19 +50,21 @@
display: inline-flex;
align-items: center;
gap: 4px;
}

[data-scope='tags-input'][data-part='item-preview'][hidden] {
display: none !important;
}
[data-scope='tags-input'][data-part='item-preview'][data-highlighted] {
background-color: #777;
border-color: #777;
color: #eee;
}
[data-scope='tags-input'][data-part='item-preview'][data-disabled] {
opacity: 0.6;
cursor: default;
&[hidden] {
display: none !important;
}

&[data-highlighted] {
background-color: #777;
border-color: #777;
color: #eee;
}

&[data-disabled] {
opacity: 0.6;
cursor: default;
}
}

[data-scope='tags-input'][data-part*='input'] {
Expand All @@ -70,24 +78,34 @@
font-size: 100%;
outline: none;
display: inline-block !important;
}

[data-scope='tags-input'][data-part*='input'][hidden] {
display: none !important;
}
&[hidden] {
display: none !important;
}

[data-scope='tags-input'][data-part*='input']:disabled {
opacity: 0.6;
&:disabled {
opacity: 0.6;
}
}

[data-scope='tags-input'][data-part='item-delete-trigger'] {
all: unset;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 4px;
background: #eee;
color: #444;
}

[data-scope='tags-input'][data-part='clear-trigger'] {
position: absolute;
top: 0;
right: 0.5px;
bottom: 0;
scale: 0.8;
display: inline-flex;
align-items: center;
justify-content: center;

svg {
scale: 1.2;
}
}
530 changes: 119 additions & 411 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"check:anatomy": "bun scripts check:anatomy",
"check:exports": "bun scripts check:exports",
"check:zag": "bun scripts check:zag",
"clean": "find . -name 'node_modules' -type d -exec rm -rf {} +",
"clean": "find . -name 'node_modules' -type d -prune -exec rm -rf {} \\;",
"exports:sync": "bun scripts exports:sync",
"local:sync": "bun scripts local:sync",
"local:revert": "bun scripts local:sync --revert",
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/dialog/dialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export default meta
export { Basic } from './examples/basic'
export { Controlled } from './examples/controlled'
export { LazyMount } from './examples/lazy-mount'
export { RapidStateChange } from './examples/rapid-state-change'
export { RenderFn } from './examples/render-fn'
export { RootProvider } from './examples/root-provider'
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { useState } from 'react'

const promise1 = Promise.resolve()
const promise2 = Promise.resolve()

export const RapidStateChange = () => {
const [open, setOpen] = useState(false)

const handleClick = async () => {
setOpen(true)
await promise1
setOpen(false)
await promise2
setOpen(true)
}

return (
<>
<button type="button" onClick={handleClick}>
Open Dialog {String(open)}
</button>
<Dialog.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>
This dialog tests the fix for rapid state changes (true → false → true). If the fix works, the dialog
should be open after clicking the button.
</Dialog.Description>
<Dialog.CloseTrigger onClick={() => setOpen(false)}>Close</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</>
)
}
9 changes: 7 additions & 2 deletions packages/react/src/components/tags-input/examples/basic.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TagsInput } from '@ark-ui/react/tags-input'
import { XIcon } from 'lucide-react'

export const Basic = () => {
return (
Expand All @@ -12,14 +13,18 @@ export const Basic = () => {
<TagsInput.Item key={index} index={index} value={value}>
<TagsInput.ItemPreview>
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>Delete</TagsInput.ItemDeleteTrigger>
<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
</TagsInput.ItemPreview>
<TagsInput.ItemInput />
</TagsInput.Item>
))}
</TagsInput.Control>
<TagsInput.Input placeholder="Add Framework" />
<TagsInput.ClearTrigger>Clear all</TagsInput.ClearTrigger>
<TagsInput.ClearTrigger>
<XIcon />
</TagsInput.ClearTrigger>
</>
)}
</TagsInput.Context>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TagsInput } from '@ark-ui/react/tags-input'
import { XIcon } from 'lucide-react'

export const BlurBehavior = () => {
return (
Expand All @@ -10,14 +11,20 @@ export const BlurBehavior = () => {
<TagsInput.Control>
{tagsInput.value.map((value, index) => (
<TagsInput.Item key={index} index={index} value={value}>
<TagsInput.ItemPreview>
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
</TagsInput.ItemPreview>
<TagsInput.ItemInput />
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>Delete</TagsInput.ItemDeleteTrigger>
</TagsInput.Item>
))}
</TagsInput.Control>
<TagsInput.Input placeholder="Add Framework" />
<TagsInput.ClearTrigger>Clear all</TagsInput.ClearTrigger>
<TagsInput.ClearTrigger>
<XIcon />
</TagsInput.ClearTrigger>
</>
)}
</TagsInput.Context>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TagsInput } from '@ark-ui/react/tags-input'
import { XIcon } from 'lucide-react'

export const DisabledEditing = () => {
return (
Expand All @@ -10,14 +11,20 @@ export const DisabledEditing = () => {
<TagsInput.Control>
{tagsInput.value.map((value, index) => (
<TagsInput.Item key={index} index={index} value={value}>
<TagsInput.ItemPreview>
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
</TagsInput.ItemPreview>
<TagsInput.ItemInput />
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>Delete</TagsInput.ItemDeleteTrigger>
</TagsInput.Item>
))}
</TagsInput.Control>
<TagsInput.Input placeholder="Add Framework" />
<TagsInput.ClearTrigger>Clear all</TagsInput.ClearTrigger>
<TagsInput.ClearTrigger>
<XIcon />
</TagsInput.ClearTrigger>
</>
)}
</TagsInput.Context>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TagsInput } from '@ark-ui/react/tags-input'
import { XIcon } from 'lucide-react'

export const InitialValue = () => {
return (
Expand All @@ -10,14 +11,20 @@ export const InitialValue = () => {
<TagsInput.Control>
{tagsInput.value.map((value, index) => (
<TagsInput.Item key={index} index={index} value={value}>
<TagsInput.ItemPreview>
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
</TagsInput.ItemPreview>
<TagsInput.ItemInput />
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>Delete</TagsInput.ItemDeleteTrigger>
</TagsInput.Item>
))}
</TagsInput.Control>
<TagsInput.Input placeholder="Add tag" />
<TagsInput.ClearTrigger>Clear all</TagsInput.ClearTrigger>
<TagsInput.ClearTrigger>
<XIcon />
</TagsInput.ClearTrigger>
</>
)}
</TagsInput.Context>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TagsInput } from '@ark-ui/react/tags-input'
import { XIcon } from 'lucide-react'

export const MaxWithOverflow = () => {
return (
Expand All @@ -10,14 +11,20 @@ export const MaxWithOverflow = () => {
<TagsInput.Control>
{tagsInput.value.map((value, index) => (
<TagsInput.Item key={index} index={index} value={value}>
<TagsInput.ItemPreview>
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>
<XIcon />
</TagsInput.ItemDeleteTrigger>
</TagsInput.ItemPreview>
<TagsInput.ItemInput />
<TagsInput.ItemText>{value}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger>Delete</TagsInput.ItemDeleteTrigger>
</TagsInput.Item>
))}
</TagsInput.Control>
<TagsInput.Input placeholder="Add Framework" />
<TagsInput.ClearTrigger>Clear all</TagsInput.ClearTrigger>
<TagsInput.ClearTrigger>
<XIcon />
</TagsInput.ClearTrigger>
</>
)}
</TagsInput.Context>
Expand Down
Loading