diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..02aa60f --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,118 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json + +language: en-US +tone_instructions: >- + Be direct and concise. Use imperative mood. Skip filler + and marketing language. Assume the reader is a competent + developer. State facts, not opinions. + +reviews: + profile: chill + instructions: >- + Do not report minor severity issues or formatting nits (markdown structure, + heading levels, trailing punctuation, blank lines in blockquotes). Focus on + critical and major issues that affect correctness, security, or public API + behavior. + sequence_diagrams: true + poem: false + auto_review: + enabled: true + drafts: false + auto_incremental_review: true + auto_pause_after_reviewed_commits: 0 + ignore_title_keywords: + - 'WIP' + - 'DO NOT MERGE' + base_branches: + - main + - dev + + # Never edit PR title or description. Reviews go in comments only. + high_level_summary: false + high_level_summary_placeholder: '' + auto_title_placeholder: '' + collapse_walkthrough: true + + path_filters: + - '!pnpm-lock.yaml' + - '!**/*.snap' + - '!docs/public/**' + - '!playground/**' + - '!playground-astro/**' + + path_instructions: + - path: 'packages/notivue/core/**' + instructions: | + This is the core notification engine. Pay close attention to: + - Reactivity correctness (Vue 3 reactive/ref usage) + - Type safety (strict TypeScript) + - Public API surface: any change here affects all consumers + - Backwards compatibility of exported types and functions + - path: 'packages/notivue/Notivue/**' + instructions: | + Main wrapper component with aria-live region for accessibility. + Ensure ARIA attributes and live region behavior are correct. + - path: 'packages/notivue/Notifications/**' + instructions: | + Pre-built notification components and themes. + Check CSS variable usage, theme consistency, and icon accessibility. + - path: 'packages/notivue/nuxt/**' + instructions: | + Nuxt module integration. Verify module hooks, auto-imports, + and compatibility with Nuxt 4 conventions. + - path: 'packages/notivue/astro/**' + instructions: | + Astro integration. Ensure SSR/island compatibility + and correct lifecycle handling. + - path: 'tests/**' + instructions: | + Test suite using Vitest (unit) and Cypress (component). + Includes accessibility testing via cypress-axe. + Ensure tests are deterministic and cover edge cases. + - path: 'docs/**' + instructions: | + VitePress documentation. Check code examples for accuracy, + correct imports, and consistency with the actual API. + + tools: + eslint: + enabled: false + biome: + enabled: false + markdownlint: + enabled: false + actionlint: + enabled: true + gitleaks: + enabled: true + + finishing_touches: + docstrings: + enabled: false + unit_tests: + enabled: false + simplify: + enabled: false + +chat: + auto_reply: true + allow_non_org_members: false + +code_generation: + unit_tests: + path_instructions: + - path: 'tests/**' + instructions: | + This project uses Cypress component tests as the primary + test suite, not unit tests. Include cypress-axe accessibility + checks where relevant. Use cypress-real-events for user + interaction testing. A small Vitest suite exists in + tests/config/ for configuration logic only. + +knowledge_base: + learnings: + scope: local + web_search: + enabled: true + code_guidelines: + enabled: true diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..4401f7f --- /dev/null +++ b/.cursorignore @@ -0,0 +1,2 @@ +!docs/ +!.claude/ \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f31a6f5..8a6a5cb 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1 @@ -polar: smastrom buy_me_a_coffee: smastrom diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a5c0953..b1c5c4e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,9 +5,13 @@ on: tags: ['v*'] workflow_dispatch: +permissions: {} + jobs: tests-workflow: uses: ./.github/workflows/tests.yml + permissions: + contents: read publish: needs: tests-workflow runs-on: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a4f2876..2737e2f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,13 +4,18 @@ on: pull_request: branches: - main + - dev push: branches: - main + - dev tags-ignore: - '*' workflow_call: +permissions: + contents: read + jobs: cypress-run: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 00e1d7a..810a051 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ pnpm-debug.log* .vscode .idea +.cursor .DS_Store node_modules @@ -13,3 +14,5 @@ notivue-*.tgz .nuxt pnpm-lock.yaml +docs/ +.claude/ diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..c5d3d6e --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 100, + "semi": false, + "singleQuote": true, + "tabWidth": 3, + "trailingComma": "es5", + "useTabs": false, + "ignorePatterns": [".vscode/**", "node_modules/**", "dist/**", "target/**"], + "sortPackageJson": { + "sortScripts": true + }, + "overrides": [ + { + "files": ["*.md"], + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..0737ecb --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,93 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "oxc"], + "jsPlugins": ["./oxlint-plugins.mjs"], + "categories": { + "correctness": "off", + "perf": "off", + "restriction": "off", + "suspicious": "off", + "pedantic": "off", + "style": "off", + "nursery": "off" + }, + "rules": { + "stylistic/sort-imports": [ + "error", + { + "internalPattern": ["^@/.+"], + "newlinesBetween": 1, + "customGroups": [ + { "groupName": "vue-files", "elementNamePattern": "\\.vue$" }, + { "groupName": "constants", "elementNamePattern": "/constants(/|$)" } + ], + "groups": [ + "vue-files", + "type", + "builtin", + "external", + "internal", + "constants", + ["parent", "sibling", "index"], + "side-effect", + "style", + "unknown" + ] + } + ], + "stylistic/padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": "import", "next": "*" }, + { "blankLine": "any", "prev": "import", "next": "import" }, + { + "blankLine": "always", + "prev": ["block-like", "if", "for", "while", "do", "switch", "try"], + "next": "*" + }, + { + "blankLine": "always", + "prev": "*", + "next": ["block-like", "if", "for", "while", "do", "switch", "try"] + }, + { + "blankLine": "any", + "prev": ["block-like", "if", "for", "while", "do", "switch", "try"], + "next": ["block-like", "if", "for", "while", "do", "switch", "try"] + }, + { "blankLine": "always", "prev": "expression", "next": ["const", "let"] }, + { "blankLine": "always", "prev": ["const", "let"], "next": "expression" }, + { "blankLine": "always", "prev": "const", "next": "let" }, + { "blankLine": "always", "prev": "let", "next": "const" }, + { + "blankLine": "always", + "prev": [ + { "selector": "ExpressionStatement[expression.type='AssignmentExpression']" }, + { "selector": "ExpressionStatement[expression.type='UpdateExpression']" } + ], + "next": [ + { "selector": "ExpressionStatement[expression.type='CallExpression']" }, + { "selector": "ExpressionStatement[expression.type='AwaitExpression']" } + ] + }, + { + "blankLine": "always", + "prev": [ + { "selector": "ExpressionStatement[expression.type='CallExpression']" }, + { "selector": "ExpressionStatement[expression.type='AwaitExpression']" } + ], + "next": [ + { "selector": "ExpressionStatement[expression.type='AssignmentExpression']" }, + { "selector": "ExpressionStatement[expression.type='UpdateExpression']" } + ] + }, + { + "blankLine": "always", + "prev": "*", + "next": ["return", "throw", "break", "continue"] + } + ] + }, + "env": { + "builtin": true + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ecec947 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# Agents + +## Project + +Notivue is a public OSS Vue 3 toast notification library. Treat `packages/notivue` as the product, with Nuxt and Astro integrations, playgrounds for manual checks, docs for the public contract, and `tests` as the regression suite. + +- Keep changes small and library-grade. Prefer existing APIs, file layout, and naming over new patterns. +- Protect public exports, types, CSS entrypoints, accessibility behavior, SSR compatibility, and backwards compatibility unless the user explicitly asks for a breaking change. +- For behavior changes, update or add focused tests near the affected area. Cypress component tests are the main suite; Vitest is used for config logic. +- Never run development servers unless the user explicitly requests them. +- Use `pnpm` and the repo scripts: `pnpm build`, `pnpm test`, `pnpm test:unit`, `pnpm format:check`, `pnpm dev`, `pnpm dev:astro`. + +## Notivue stream layout + +- Notification rows use `width: max-content` on `
  • ` so keyboard focus rings hug the toast. Stream focus and `aria-label` belong on the `
  • `; `[data-notivue-item]` is not focusable. +- Horizontal alignment follows `position` (`*-left`, `*-center`, `*-right`). Responsive changes use `config.update` / `updateConfig`, not CSS alignment overrides. +- `--nv-gap` is block-end margin on list items. Stacking uses `getListItemStackHeight` in `packages/notivue/core/utils.ts`. +- `NotivueKeyboard` `isCandidate` receives the `
  • ` (`[data-notivue-list-item]`), same element as stream focus. + +## Workflow + +- **GitHub (PR review):** CodeRabbit comments on pull requests. Triage each thread: fix when valid, dismiss with a short reason when not. Resolve threads after replying. +- **Local (implementation):** Cursor Agent makes changes, commits, and pushes on request. When answering CodeRabbit on GitHub, post replies on the review thread and note they were written by the agent (see below). +- Do not create commits or push to remotes unless the user explicitly requests it in the current conversation. +- After implementing changes, stop and let the user review the diff before committing or pushing. Do not commit proactively at the end of a task unless they asked for it in that message. +- **Before pushing:** If the branch has an open PR, run `gh pr view` and update the PR title and body when new commits change scope, behavior, dependencies, or the test plan. Do not push with an outdated description. + +## Writing + +- Do not use em-dashes. Use a hyphen, a comma, a colon, or separate sentences instead. + +## Git + +Match the style on `main`. Do **not** use Conventional Commits (`feat:`, `fix:`, `chore:`, etc.). + +**Titles** + +- Scoped: `{Area} - {Description}`. Capitalize the part after `-`. + - Areas: `Core`, `Notivue`, `Notification`, `Notifications`, `Pkg`, `Demo`, `Playground`, `Astro`, `Tests`, `Docs`, `CodeRabbit`, … + - Examples: `Core - Rename useRepositioning to useSizes`, `NotivueKeyboard - Fix focusable element tabindex selector` +- Multi-scope: `Core, Astro - Cleanup` +- Releases: `2.4.5 (#63)` (include PR number for version bumps) +- Small / misc: short imperative is fine (e.g. `Remove Claude Code GitHub workflow`) + +**Body (optional)** + +- Bullet lines: `* {Area} - …` + +**Agent commits** + +- Append co-author trailer: + + ```text + Co-authored-by: Cursor + ``` + +**GitHub review replies** + +- When replying to CodeRabbit (or any PR comment) on the user's behalf, end with: + + ```text + Added by Cursor Agent + ``` diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 96a930b..2e5d313 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,24 +17,24 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the - overall community +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community Examples of unacceptable behavior include: -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Enforcement Responsibilities diff --git a/README.md b/README.md index 1fbb5bf..0b0e848 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,6 @@ -
    - -notivue - -
    - # Notivue -### Powerful toast notification system for Vue and Nuxt +**Zero-dependency, powerful, and truly customizable toast notification system for Vue** [Live Demo](https://notivue.smastrom.io) - [Documentation](https://docs.notivue.smastrom.io) @@ -16,43 +10,19 @@ [Nuxt](https://stackblitz.com/edit/nuxt-starter-fnhcmx?file=pages%2Findex.vue) - [Astro](https://stackblitz.com/edit/withastro-astro-qyesvk?file=src%2Fcomponents%2FVueComponent.vue) -
    - -
    - -
    - ## Features -**🧬 Fully modular with zero dependencies** -_Granularly include only the features you need_ - -**✅ Beautiful, ready-made notifications included** -_Themes, icons, progress bar, and native RTL support_ - -**🧩 Headless API** -_Use your own components while Notivue handles the rest_ - -**💊 Drop-in components to enhance notifications** -_NotivueSwipe, NotivueKeyboard, all optional and customizable_ - -**🌀 Dynamic Notifications** -_Update pending notifications with a breeze_ - -**🎢 Slick transitions and animations** -_Customize animations with CSS classes_ - -**♿️ Fully accessible** -_Built-in announcements, reduced-motion and keyboard support_ - -**💫 Nuxt and Astro modules** -_Built-in Nuxt and Astro ad-hoc modules_ - -
    +- 🧬 **Modular, zero dependencies** - Only what you import reaches your bundle +- ✅ **Ready-made notifications** - CSS-variable themes with dark mode, progress bars, and RTL support. +- 🧩 **Headless API** - Bring your own components. Notivue handles the stack and lifecycle. +- 🌀 **Promise API** - Update a notification from _loading_ to _success_ or _error_ with a reactive handle. +- ♿️ **Accessible** - Live regions, reduced-motion, pause-on-hover, pause-on-focus, swipe-to-dismiss, and full keyboard navigation. +- 🛡️ **Battle-tested** - 160+ E2E tests +- 💫 **Nuxt and Astro modules** - Zero-config framework integration ## Installation -```shell +```bash pnpm add notivue # npm i notivue @@ -60,11 +30,11 @@ pnpm add notivue # bun i notivue ``` -
    +> :bulb: Beyond this quick start — Nuxt/Astro setup, **`notify`** / callbacks, stream config, built-in styling, headless mode, full API — lives in the [documentation](https://docs.notivue.smastrom.io). -## Vite +## Quick start (Vite) -> :bulb: See [↓ below](#nuxt) for **Nuxt** +> :bulb: See ↓ below for [Nuxt](#nuxt) and [Astro](#astro). **main.js/ts** @@ -77,7 +47,14 @@ import App from './App.vue' import 'notivue/notification.css' // Only needed if using built-in import 'notivue/animations.css' // Only needed if using default animations -const notivue = createNotivue(/* Options */) +const notivue = createNotivue({ + position: 'bottom-right', + pauseOnHover: true, + pauseOnTabChange: true, + limit: 4, + enqueue: true, + // other options... +}) const app = createApp(App) app.use(notivue) @@ -88,11 +65,16 @@ app.mount('#app') ```vue ``` -
    -Headless, with custom components +### Swipe to dismiss (Optional) + +Wrap the notification in **`NotivueSwipe`** so toasts can be cleared with a horizontal swipe. Use the same wrapper around the built-in **`Notification`** or around a **custom component** (see the `` example below). ```vue +``` + +### Custom components — your own markup + +**components/Toast.vue** + +```vue + + + ``` -
    +**App.vue** + +```vue + + + +``` ## Nuxt @@ -158,11 +191,16 @@ export default defineNuxtConfig({ modules: ['notivue/nuxt'], css: [ 'notivue/notification.css', // Only needed if using built-in - 'notivue/animations.css' // Only needed if using default animations + 'notivue/animations.css', // Only needed if using default animations ], notivue: { - // Options - } + position: 'bottom-right', + pauseOnHover: true, + pauseOnTabChange: true, + limit: 4, + enqueue: true, + // other options... + }, }) ``` @@ -170,7 +208,12 @@ export default defineNuxtConfig({ ```vue ``` -
    +## Astro + +> :bulb: Import from **`notivue/astro`** (not `notivue`). CSS: **`notivue/astro/notification.css`** and **`notivue/astro/animations.css`** when you use the built-in UI and default animations. +> **Note:** Astro **6** is not supported yet. ## Thanks - [Ionic Team](https://ionic.io/) for the icons - [Uktash Verna](https://github.com/n3r4zzurr0) for the animated spinner -
    - ## License MIT diff --git a/astro-playground/package.json b/astro-playground/package.json deleted file mode 100644 index d6cf642..0000000 --- a/astro-playground/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "notivue-astro-playground", - "type": "module", - "private": true, - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro build", - "preview": "astro preview", - "astro": "astro" - }, - "dependencies": { - "@astrojs/react": "^3.6.2", - "@astrojs/vue": "^4.5.0", - "astro": "^4.15.1", - "notivue": "workspace:*", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "vue": "^3.4.30" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0" - } -} diff --git a/astro-playground/src/components/ReactComponent.tsx b/astro-playground/src/components/ReactComponent.tsx deleted file mode 100644 index 540d085..0000000 --- a/astro-playground/src/components/ReactComponent.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { push } from 'notivue/astro' - -function pushStatic() { - push.info({ - title: 'React Notification', - message: 'Notification from React!', - }) -} - -function pushPromise() { - const promise = push.promise({ - title: 'Loading React Notification...', - message: 'Loading notification from React...', - }) - - setTimeout(() => { - promise.resolve({ - title: 'React Notification', - message: 'Loaded notification from React!', - }) - }, 2000) -} - -export function ReactComponent() { - return ( -
    -

    From React

    - - - - - -
    - ) -} diff --git a/astro-playground/src/components/VueComponent.vue b/astro-playground/src/components/VueComponent.vue deleted file mode 100644 index 1cf8ebf..0000000 --- a/astro-playground/src/components/VueComponent.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/oxlint-plugins.mjs b/oxlint-plugins.mjs new file mode 100644 index 0000000..60c7c7a --- /dev/null +++ b/oxlint-plugins.mjs @@ -0,0 +1,13 @@ +import stylistic from '@stylistic/eslint-plugin' +import perfectionist from 'eslint-plugin-perfectionist' + +const paddingLineBetweenStatements = stylistic.rules['padding-line-between-statements'] +const sortImports = perfectionist.rules['sort-imports'] + +export default { + meta: { name: 'stylistic' }, + rules: { + 'padding-line-between-statements': paddingLineBetweenStatements, + 'sort-imports': sortImports, + }, +} diff --git a/package.json b/package.json index b7c6044..a262997 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,51 @@ { "name": "notivue-monorepo", "private": true, - "packageManager": "pnpm@8.14.3", - "engines": { - "node": ">=20.0.0" - }, "scripts": { - "dev": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C playground install && pnpm -C playground run dev --host\"", - "dev:astro": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C astro-playground install && pnpm -C astro-playground run dev --host\"", "build": "pnpm -C packages/notivue run build", "build:playground": "pnpm build && pnpm install && pnpm -C playground run build", + "dev": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C playground run dev --host\"", + "dev:astro": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C playground-astro run dev --host\"", + "docs:build": "pnpm -C docs run build", + "docs:dev": "pnpm -C docs run dev", + "docs:preview": "pnpm -C docs run preview", + "format": "oxfmt . && pnpm -C playground-astro exec prettier --write \"src/**/*.astro\" && oxlint --fix .", + "format:check": "oxfmt --check . && oxlint .", + "prepare": "husky", "test": "pnpm build && pnpm install && pnpm -C tests run test", - "test:gui": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C tests install && pnpm -C tests run test:gui\"", - "test:unit": "pnpm -C tests run test:unit", - "prepare": "husky" + "test:gui": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C tests run test:gui\"", + "test:unit": "pnpm -C tests run test:unit" }, "devDependencies": { - "concurrently": "^8.2.2", - "husky": "^9.1.5", - "lint-staged": "^15.2.9", - "prettier": "^3.3.3", - "prettier-plugin-astro": "^0.14.1" + "@stylistic/eslint-plugin": "^5.10.0", + "concurrently": "^9.1.2", + "eslint-plugin-perfectionist": "^5.9.0", + "husky": "^9.1.7", + "lint-staged": "^15.4.3", + "oxfmt": "^0.42.0", + "oxlint": "^1.67.0" }, "lint-staged": { - "*.{js,ts,vue,json,css,md}": "prettier --write" + "*.{js,ts,mts,cts,mjs,cjs,vue}": [ + "oxfmt --write", + "oxlint --fix" + ], + "*.{json,css,md,html}": "oxfmt --write", + "*.astro": [ + "pnpm -C playground-astro exec prettier --write --config .prettierrc", + "oxlint --fix" + ] + }, + "engines": { + "node": ">=20.19.0" + }, + "packageManager": "pnpm@9.15.9", + "pnpm": { + "onlyBuiltDependencies": [ + "@parcel/watcher", + "cypress", + "esbuild", + "sharp" + ] } } diff --git a/packages/notivue/Notifications/Notification.vue b/packages/notivue/Notifications/Notification.vue index a268d61..3750bdc 100644 --- a/packages/notivue/Notifications/Notification.vue +++ b/packages/notivue/Notifications/Notification.vue @@ -1,10 +1,10 @@ diff --git a/packages/notivue/NotivueKeyboard/constants.ts b/packages/notivue/NotivueKeyboard/constants.ts index c80d65f..cb399eb 100644 --- a/packages/notivue/NotivueKeyboard/constants.ts +++ b/packages/notivue/NotivueKeyboard/constants.ts @@ -1,16 +1,12 @@ -import type { InjectionKey } from 'vue' -import type { NotivueKeyboardData } from 'notivue' - -export const keyboardInjectionKey = Symbol('') as InjectionKey - export const focusableEls = - 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]' + 'a[href]:not([tabindex^="-"]), area[href]:not([tabindex^="-"]), input:not([disabled]):not([tabindex^="-"]), select:not([disabled]):not([tabindex^="-"]), textarea:not([disabled]):not([tabindex^="-"]), button:not([disabled]):not([tabindex^="-"]), [tabindex]:not([tabindex^="-"])' export const DEFAULT_PROPS = { comboKey: 'n', handleClicks: true, - leaveMessage: "You're leaving the notifications stream. Press Control + N to navigate it again.", + leaveMessage: + 'You left the notifications stream. Press Tab or Control + N to navigate it again.', emptyMessage: 'No notifications to navigate', - renderAnnouncement: true, + renderAnnouncement: false, maxAnnouncements: 2, } as const diff --git a/packages/notivue/NotivueKeyboard/types.ts b/packages/notivue/NotivueKeyboard/types.ts index 36f218d..78e8b89 100644 --- a/packages/notivue/NotivueKeyboard/types.ts +++ b/packages/notivue/NotivueKeyboard/types.ts @@ -1,26 +1,30 @@ import type { Component, ComputedRef, Ref } from 'vue' +/** @deprecated No longer used. */ export type TabIndexValue = 0 | -1 + +/** @deprecated No longer used. */ export type ContainersTabIndexMap = Record +/** @deprecated No longer used. `useNotivueKeyboard()` is no longer needed. */ export interface NotivueKeyboardData { - /** - * Reactive tab index value for the custom notification focusable elements tabindex. - * - * Meant to be added to the custom notification elements via `:tabindex="elementsTabIndex"`. - */ elementsTabIndex: Ref - /** - * Reactive map of tab index values for the notification containers. - * - * Meant to be passed as prop to Notivue via `:containersTabIndex="containersTabIndex"`. - */ containersTabIndex: ComputedRef } export interface NotivueKeyboardProps { /** - * Key to combine with Shift to enter or exit the stream. + * Custom function to determine if a notification list item is a candidate for + * keyboard navigation. Receives the `
  • ` element (`[data-notivue-list-item]`). + * + * By default, a list item is a candidate if it contains at least one focusable + * element (buttons, links, inputs, selects, textareas, or elements with a non-negative `tabindex`). + * + * @default undefined + */ + isCandidate?: (el: HTMLElement) => boolean + /** + * Key to combine with Control to enter or exit the stream. * * @default "n" */ @@ -33,9 +37,9 @@ export interface NotivueKeyboardProps { */ handleClicks?: boolean /** - * Text to be announced when leaving the stream + * Text to be announced when leaving the stream. * - * @default "You're leaving the notifications stream. Press Control + N to navigate it again." + * @default "You left the notifications stream. Press Tab or Control + N to navigate it again." */ leaveMessage?: string /** @@ -47,7 +51,7 @@ export interface NotivueKeyboardProps { /** * Whether to render the enter/leave notification or just announce it via screen reader. * - * @default true + * @default false */ renderAnnouncement?: boolean /** @@ -59,8 +63,5 @@ export interface NotivueKeyboardProps { } export interface NotivueKeyboardSlot { - default(props: { - elementsTabIndex: TabIndexValue - containersTabIndex: ContainersTabIndexMap - }): Component + default(): Component } diff --git a/packages/notivue/NotivueKeyboard/useNotivueKeyboard.ts b/packages/notivue/NotivueKeyboard/useNotivueKeyboard.ts index 2848095..a0c92c5 100644 --- a/packages/notivue/NotivueKeyboard/useNotivueKeyboard.ts +++ b/packages/notivue/NotivueKeyboard/useNotivueKeyboard.ts @@ -1,17 +1,14 @@ -import { inject, ref, computed } from 'vue' - -import { keyboardInjectionKey } from './constants' -import { isSSR } from '@/core/utils' - import type { NotivueKeyboardData } from 'notivue' +import { ref, computed } from 'vue' + +/** + * @deprecated NotivueKeyboard now manages focus automatically. + * This composable is no longer needed — remove it from your code. + */ export function useNotivueKeyboard(): NotivueKeyboardData { - if (isSSR) { - return { - elementsTabIndex: ref(-1), - containersTabIndex: computed(() => ({})), - } + return { + elementsTabIndex: ref(-1), + containersTabIndex: computed(() => ({})), } - - return inject(keyboardInjectionKey) as NotivueKeyboardData } diff --git a/packages/notivue/NotivueSwipe/NotivueSwipe.vue b/packages/notivue/NotivueSwipe/NotivueSwipe.vue index ec27d83..d79c5fd 100644 --- a/packages/notivue/NotivueSwipe/NotivueSwipe.vue +++ b/packages/notivue/NotivueSwipe/NotivueSwipe.vue @@ -1,4 +1,6 @@ + + diff --git a/astro-playground/src/env.d.ts b/playground-astro/src/env.d.ts similarity index 100% rename from astro-playground/src/env.d.ts rename to playground-astro/src/env.d.ts diff --git a/astro-playground/src/layouts/Layout.astro b/playground-astro/src/layouts/Layout.astro similarity index 99% rename from astro-playground/src/layouts/Layout.astro rename to playground-astro/src/layouts/Layout.astro index 7b1b79e..e22a568 100644 --- a/astro-playground/src/layouts/Layout.astro +++ b/playground-astro/src/layouts/Layout.astro @@ -1,11 +1,10 @@ --- -import { ViewTransitions } from 'astro:transitions' - import Notivue from '@/components/Notivue.vue' +import { ViewTransitions } from 'astro:transitions' + import 'notivue/astro/notification.css' import 'notivue/astro/animations.css' - import '@/styles/reset.css' --- diff --git a/astro-playground/src/pages/_app.ts b/playground-astro/src/pages/_app.ts similarity index 100% rename from astro-playground/src/pages/_app.ts rename to playground-astro/src/pages/_app.ts index 2a74a27..35f02cf 100644 --- a/astro-playground/src/pages/_app.ts +++ b/playground-astro/src/pages/_app.ts @@ -1,7 +1,7 @@ -import { createNotivue } from 'notivue/astro' - import type { App, Plugin } from 'vue' +import { createNotivue } from 'notivue/astro' + const notivue = createNotivue({ teleportTo: '#notivue_teleport', }) diff --git a/astro-playground/src/pages/about.astro b/playground-astro/src/pages/about.astro similarity index 100% rename from astro-playground/src/pages/about.astro rename to playground-astro/src/pages/about.astro index 4e4741c..ad4fe92 100644 --- a/astro-playground/src/pages/about.astro +++ b/playground-astro/src/pages/about.astro @@ -1,9 +1,9 @@ --- -import Layout from '@/layouts/Layout.astro' - import VueComponent from '@/components/VueComponent.vue' + import AstroComponent from '@/components/AstroComponent.astro' import { ReactComponent } from '@/components/ReactComponent' +import Layout from '@/layouts/Layout.astro' --- diff --git a/astro-playground/src/pages/index.astro b/playground-astro/src/pages/index.astro similarity index 99% rename from astro-playground/src/pages/index.astro rename to playground-astro/src/pages/index.astro index 18f02c9..bdcd9ad 100644 --- a/astro-playground/src/pages/index.astro +++ b/playground-astro/src/pages/index.astro @@ -1,9 +1,10 @@ --- -import Layout from '../layouts/Layout.astro' - import VueComponent from '../components/VueComponent.vue' + import AstroComponent from '@/components/AstroComponent.astro' + import { ReactComponent } from '../components/ReactComponent' +import Layout from '../layouts/Layout.astro' --- diff --git a/astro-playground/src/styles/reset.css b/playground-astro/src/styles/reset.css similarity index 93% rename from astro-playground/src/styles/reset.css rename to playground-astro/src/styles/reset.css index ded5d7d..7e3fc53 100644 --- a/astro-playground/src/styles/reset.css +++ b/playground-astro/src/styles/reset.css @@ -8,7 +8,8 @@ } html { - font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', + font-family: + system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; } diff --git a/astro-playground/tsconfig.json b/playground-astro/tsconfig.json similarity index 100% rename from astro-playground/tsconfig.json rename to playground-astro/tsconfig.json diff --git a/playground/app.vue b/playground/app.vue index f3ca66b..ed73136 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -1,17 +1,15 @@ diff --git a/playground/components/nav/NavNotivuePosition.vue b/playground/components/nav/NavNotivuePosition.vue index dea14f6..50c7932 100644 --- a/playground/components/nav/NavNotivuePosition.vue +++ b/playground/components/nav/NavNotivuePosition.vue @@ -43,14 +43,6 @@ const btnProps = { - diff --git a/playground/components/nav/NavPushBuiltIn.vue b/playground/components/nav/NavPushBuiltIn.vue index ae2f146..5503ccd 100644 --- a/playground/components/nav/NavPushBuiltIn.vue +++ b/playground/components/nav/NavPushBuiltIn.vue @@ -3,27 +3,29 @@ const { state, messages } = useStore() async function asyncRefMessagePush() { const initialMessage = ref(state.rtl ? 'جاري تحميل الملفات...' : 'Preparing to upload files...') - const notification = push.promise(initialMessage) + const notification = notify.loading(initialMessage) for (let n = 1; n < 4; n++) { await new Promise((resolve) => setTimeout(resolve, getRandomInt(1000, 2000))) + initialMessage.value = state.rtl ? `جاري تحميل الملف ${n}/3...` : `Uploading file ${n}/3...` } await new Promise((resolve) => setTimeout(resolve, getRandomInt(1000, 2000))) - notification.resolve(state.rtl ? 'تم تحميل جميع الملفات!' : 'All files uploaded!') + notification.success(state.rtl ? 'تم تحميل جميع الملفات!' : 'All files uploaded!') } async function asyncPush() { if (Math.random() > 0.7) return asyncRefMessagePush() - const promise = push.promise(messages.value.promise) + const notification = notify.loading(messages.value.dynamic) + await new Promise((resolve) => setTimeout(resolve, getRandomInt(2000, 4000))) if (Math.random() > 0.5) { - promise.resolve(messages.value.success) + notification.success(messages.value.success) } else { - promise.reject(messages.value.error) + notification.error(messages.value.error) } } @@ -31,13 +33,16 @@ async function asyncPush() { diff --git a/playground/components/nav/NavPushHeadless.vue b/playground/components/nav/NavPushHeadless.vue index dad4ef1..4e50a2d 100644 --- a/playground/components/nav/NavPushHeadless.vue +++ b/playground/components/nav/NavPushHeadless.vue @@ -1,9 +1,11 @@