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
5 changes: 5 additions & 0 deletions .changeset/happy-streets-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vingy/vueltip": patch
---

Prefix default attributes with `data`
5 changes: 5 additions & 0 deletions .changeset/rich-maps-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vingy/vueltip": major
---

Breaking: rename 'content' to 'text'
5 changes: 5 additions & 0 deletions .changeset/ten-pugs-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vingy/vueltip": minor
---

Add custom data to directive to enable complex tooltips
28 changes: 21 additions & 7 deletions .github/skills/common-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,25 +125,38 @@ export const vueltipPlugin = {
**directive.ts:** Handles lifecycle + event listener setup/teardown

```typescript
const LISTENERS: [
event: string,
handler: EventListener,
][] = [
['eventA', onEnter],
['eventB', onLeave],
]

export const vueltipDirective = {
created: (el, binding) => {
const key = generateKey()
setContent(key, toContent(binding.value))
el.setAttribute(getOption('keyAttribute'), key)
setContent(key, toContent(binding.value)) // Module-level state
el.addEventListener('mouseenter', onMouseover) // Stored reference
el.addEventListener('mouseleave', onMouseout)
for (const [event, handler] of LISTENERS) {
el.addEventListener(event, handler)
}
},
updated: (el, binding) => {
// Re-sync content/placement on binding change
// Re-sync state/attributes on binding change
},
beforeUnmount: (el) => {
deleteContent(el.getAttribute(getOption('keyAttribute')))
el.removeEventListener('mouseenter', onMouseover) // Must match
el.removeEventListener('mouseleave', onMouseout)
ensureKey(el, (key) => deleteContent(key))
for (const [event, handler] of LISTENERS) {
el.removeEventListener(event, handler)
}
},
}
```

**Durability note:** Keep this as a lifecycle template.
Event names and attribute defaults can evolve.

### Composable Structure

**composables.ts:** Exposes floating UI + state binding for template
Expand Down Expand Up @@ -329,6 +342,7 @@ Enforces:
- [ ] `pnpm test` passes
- [ ] `pnpm lint` passes
- [ ] `pnpm format` run
- [ ] `pnpm typecheck` passes
- [ ] Tests added for new code
- [ ] `pnpm changeset` created
- [ ] Demo updated (if user-facing)
Expand Down
14 changes: 14 additions & 0 deletions .github/skills/instruction-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| Instructions match codebase | Read source files, compare patterns | Update instructions with real code examples |
| Examples are current | Check file links still exist | Update links or remove outdated examples |
| Patterns are cohesive | Cross-reference skills for consistency | Consolidate or clarify conflicting advice |
| Instructions overfit internals | Scan for exact literals and private names | Replace with stable pattern + one concrete reference |
| Anti-patterns are clear | Scan all ❌ marked items | Ensure each has explanation and correct approach |
| Decision trees are accurate | Follow trees on real tasks | Add missing branches, remove irrelevant ones |
| Completeness coverage | Map all file types and workflows | Add missing patterns, remove duplicates |
Expand Down Expand Up @@ -81,6 +82,7 @@ cat .github/skills/state-management.md | grep -A 10 "Event Handler Wrapper"
| Same pattern explained differently | Search both skills for same keyword | Pick clearer explanation, remove duplicate |
| Contradictory advice | Search for opposing ❌ markings | Determine which is correct, remove error |
| Different terminology | Search for synonyms across skills | Standardize term usage everywhere |
| Over-specific literals | Search defaults/attribute names in docs | Keep literals only when part of public API |
| Missing links | Grep for file references | Verify all links exist, update if moved |
| Outdated examples | Check line counts match | Update example code to match current file |

Expand All @@ -105,6 +107,7 @@ wc -l packages/vueltip/src/state.ts # Should match any line ranges in examples
2. Are line numbers accurate if provided?
3. Would copying the code work as-is?
4. Are imports complete and correct?
5. Is this showing a durable pattern, not an unstable literal?

**Example check:**

Expand Down Expand Up @@ -194,6 +197,7 @@ Before committing changes to any skill:
- [ ] **Clarity**: Language is direct and unambiguous
- [ ] **Consistency**: Terms match other skills
- [ ] **Linkage**: All file links still valid
- [ ] **Durability**: Guidance survives field/default renames
- [ ] **Anti-patterns**: Each ❌ has ✅ fix shown
- [ ] **Decision trees**: All branches covered
- [ ] **Practicality**: Real-world applicability verified
Expand Down Expand Up @@ -259,6 +263,16 @@ done

## Red Flags: Patterns to Catch

**Red Flag 0: Docs mirror private internals too closely**
```
Instruction includes exact defaults and private key names
Small refactor causes many skill edits
Fix by documenting invariant behavior and linking to source
for current literals
```

**Red Flag 1: Example code doesn't compile**
```typescript
// ❌ Instruction shows:
Expand Down
26 changes: 22 additions & 4 deletions .github/skills/package-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export type { PluginOptions } from './types'
export { vueltipPlugin } from './plugin'
export { vueltipDirective } from './directive'
export { useVueltip } from './composables'
export type { Placement, Content, Options } from './types'
export type {
PublicPayload,
PublicValue,
} from './types'

// ❌ Do NOT export:
// export { hoveredElement, setContent } from './state' // Internal
Expand Down Expand Up @@ -98,7 +101,7 @@ packages/*/
| File Type | Pattern | Export? | Example |
|-----------|---------|---------|---------|
| `index.ts` | Public API | Yes, only public | Composables, plugins, directives, types |
| `types.ts` | All type definitions | Re-export public types from index.ts | `Content`, `Binding`, `Options` |
| `types.ts` | All type definitions | Re-export public types from index.ts | Public content/value/options types |
| `[feature].ts` | Feature logic | Only if public API | `debug.ts`, `directive.ts`, `plugin.ts` |
| `listeners.ts` | Event handlers | No, internal | `onMouseover`, `onMouseout` |
| `utils.ts` | Helper functions | No, internal | `isTruncated`, `isHtmlElement`, `elementContainsText` |
Expand All @@ -116,7 +119,14 @@ packages/*/
```typescript
// src/types.ts
export type Placement = 'top' | 'bottom' | 'left' | 'right'
export interface Content { text?: string }
export interface PublicPayload {
text: string | null | undefined
}
export type PublicValue =
| string
| null
| undefined
| (PublicPayload & { placement?: Placement })
export interface Options { showDelay: number }

// Keep internal types here too - they stay private unless re-exported
Expand All @@ -133,10 +143,18 @@ declare module 'vue' {

```typescript
// src/index.ts
export type { Placement, Content, Options } from './types'
export type {
PublicPayload,
PublicValue,
Options,
} from './types'
// Don't re-export internal types
```

> Prefer describing type *roles* (content/value/options,
> public vs internal) rather than locking docs to exact
> field names that may change during refactors.

---

## Dependencies
Expand Down
22 changes: 15 additions & 7 deletions .github/skills/state-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
| Type-safe event handlers | Wrapper functions | None (stored ref) | `ensureEventTarget()` |
| App configuration | Getter functions | None | `getOption()`, `setOptions()` |

> Durability rule: prefer stable patterns over exact
> literals. Treat concrete default values and exact
> attribute names as examples unless they are part of
> documented public API.

---

## Decision Tree
Expand Down Expand Up @@ -166,13 +171,13 @@ scope.run(() => {

```typescript
export const vueltipDirective = {
created: (el, binding) => {
el.addEventListener('mouseenter', onMouseover)
el.addEventListener('focus', onMouseover)
created: (el) => {
el.addEventListener('eventA', handlerA)
el.addEventListener('eventB', handlerB)
},
beforeUnmount: (el) => {
el.removeEventListener('mouseenter', onMouseover)
el.removeEventListener('focus', onMouseover)
el.removeEventListener('eventA', handlerA)
el.removeEventListener('eventB', handlerB)
},
}
```
Expand Down Expand Up @@ -270,8 +275,9 @@ export const onMouseover = ensureEventTarget((target) => {
import type { Options } from './types'

let options: Options = {
placementAttribute: 'vueltip-placement',
keyAttribute: 'vueltip-key',
placementAttribute: DEFAULT_PLACEMENT_ATTRIBUTE,
keyAttribute: DEFAULT_KEY_ATTRIBUTE,
truncateAttribute: DEFAULT_TRUNCATE_ATTRIBUTE,
showDelay: 0,
hideDelay: 200,
}
Expand All @@ -288,6 +294,8 @@ export const getOption = <T extends keyof Options>(
**Critical:**
- Provide typed getter: `getOption('showDelay')` returns `number`
- Merge partial options: `{ ...defaults, ...provided }`
- Keep defaults centralized in one module; avoid
hardcoding the same literal in multiple files/docs
- Keep internal: don't export `options` directly
- Use in composables/directives to access config

Expand Down
14 changes: 14 additions & 0 deletions .github/skills/type-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,20 @@ declare module 'vue' {
- Extend component props
- Add module augmentation

**Vueltip custom data augmentation:**

```typescript
declare module '@vingy/vueltip' {
interface CustomVueltipData {
userId?: number
severity?: 'info' | 'warning' | 'error'
}
}

// Now content.custom is strongly typed:
// v-tooltip="{ text: 'Profile', custom: { userId: 1 } }"
```

**Anti-patterns:**
- ❌ Ambient declarations for private types
- ❌ Multiple ambient declarations in different files (consolidate in types.ts)
Expand Down
17 changes: 13 additions & 4 deletions demo/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ debug('foobar', { count })
</button>
<button
v-tooltip="{
content: 'Remove one item',
text: 'Remove one item',
placement: 'right',
}"
class="inline-flex items-center gap-2 rounded-lg border border-rose-500/60 bg-rose-500/10 px-4 py-2 text-sm font-semibold text-rose-200 transition hover:bg-rose-500/20"
Expand Down Expand Up @@ -133,7 +133,7 @@ debug('foobar', { count })
</p>
<p
v-tooltip="{
content: 'Bottom placement',
text: 'Bottom placement',
placement: 'bottom',
}"
class="rounded-xl border border-slate-700 bg-slate-900/70 px-4 py-3 text-sm text-slate-200 shadow-sm transition hover:border-slate-500"
Expand All @@ -142,7 +142,7 @@ debug('foobar', { count })
</p>
<p
v-tooltip="{
content: 'Left placement',
text: 'Left placement',
placement: 'left',
}"
class="rounded-xl border border-slate-700 bg-slate-900/70 px-4 py-3 text-sm text-slate-200 shadow-sm transition hover:border-slate-500"
Expand All @@ -151,13 +151,22 @@ debug('foobar', { count })
</p>
<p
v-tooltip="{
content: 'Right placement',
text: 'Right placement',
placement: 'right',
}"
class="rounded-xl border border-slate-700 bg-slate-900/70 px-4 py-3 text-sm text-slate-200 shadow-sm transition hover:border-slate-500"
>
Hover over me (right)
</p>
<p
v-tooltip="{
text: 'Custom data: ',
custom: { sum: 23 },
}"
class="rounded-xl border border-slate-700 bg-slate-900/70 px-4 py-3 text-sm text-slate-200 shadow-sm transition hover:border-slate-500"
>
Hover over me (custom data)
</p>
</div>
<div class="mt-6 space-y-3">
<div>
Expand Down
3 changes: 3 additions & 0 deletions demo/src/Tooltip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const { tooltipStyles, arrowStyles, show, content } =
class="relative z-10 rounded bg-slate-100 px-2 py-1 text-sm font-semibold text-slate-900 shadow-lg"
>
{{ content?.text }}
<span v-if="content?.custom">
{{ JSON.stringify(content.custom) }}</span
>
</div>
</div>
</template>
6 changes: 6 additions & 0 deletions demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ app
.directive('tooltip', vueltipDirective)

app.mount('#app')

declare module '@vingy/vueltip' {
export interface CustomVueltipData {
sum: number
}
}
Comment thread
vinpogo marked this conversation as resolved.
3 changes: 1 addition & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ experimental = true

[tasks]
install = "pnpm install"
build = "pnpm build"
build = "pnpm build && pnpm typecheck"
dev = "pnpm dev"
test = "pnpm test"
oxlint = "pnpm oxlint"
oxfmt = "pnpm oxfmt --check"

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"build": "pnpm -r build",
"dev": "pnpm -r --parallel --stream dev",
"demo": "pnpm --filter demo run dev",
"typecheck": "tsc --noEmit",
"changeset": "changeset",
"changeset:version": "changeset version",
"changeset:publish": "pnpm build && changeset publish"
Expand Down
Loading