@codav/slug-generator 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -217
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +148 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +138 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,217 +1,217 @@
|
|
|
1
|
-
# @codav/slug-generator
|
|
2
|
-
|
|
3
|
-
A lightweight, reusable and TypeScript-first React component for generating clean, URL-friendly slugs directly from titles.
|
|
4
|
-
|
|
5
|
-
Built for modern React and Next.js applications with a simple API, zero unnecessary complexity, and easy integration.
|
|
6
|
-
|
|
7
|
-
## ✨ Features
|
|
8
|
-
|
|
9
|
-
- Generate slugs directly from titles
|
|
10
|
-
- Supports Persian, English, numbers and Unicode characters
|
|
11
|
-
- Automatically removes unnecessary characters
|
|
12
|
-
- Converts spaces into `-`
|
|
13
|
-
- Prevents duplicate hyphens
|
|
14
|
-
- Removes leading and trailing hyphens
|
|
15
|
-
- Fully controlled React component
|
|
16
|
-
- Written in TypeScript
|
|
17
|
-
- React 18 and React 19 compatible
|
|
18
|
-
- Next.js compatible
|
|
19
|
-
- Customizable labels and button text
|
|
20
|
-
- Supports custom classes
|
|
21
|
-
- Lightweight package architecture
|
|
22
|
-
- ESM and CommonJS builds
|
|
23
|
-
- Built-in TypeScript declarations
|
|
24
|
-
|
|
25
|
-
## 📦 Installation
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm install @codav/slug-generator
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Or:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
yarn add @codav/slug-generator
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
pnpm add @codav/slug-generator
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## 🚀 Basic Usage
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
"use client";
|
|
45
|
-
|
|
46
|
-
import { useState } from "react";
|
|
47
|
-
import { SlugGenerator } from "@codav/slug-generator";
|
|
48
|
-
|
|
49
|
-
export default function Example() {
|
|
50
|
-
const [title, setTitle] = useState("");
|
|
51
|
-
const [slug, setSlug] = useState("");
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<SlugGenerator
|
|
55
|
-
title={title}
|
|
56
|
-
value={slug}
|
|
57
|
-
onChange={setSlug}
|
|
58
|
-
/>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## 🧩 With a Title Input
|
|
64
|
-
|
|
65
|
-
```tsx
|
|
66
|
-
"use client";
|
|
67
|
-
|
|
68
|
-
import { useState } from "react";
|
|
69
|
-
import { SlugGenerator } from "@codav/slug-generator";
|
|
70
|
-
|
|
71
|
-
export default function BlogForm() {
|
|
72
|
-
const [title, setTitle] = useState("");
|
|
73
|
-
const [slug, setSlug] = useState("");
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div>
|
|
77
|
-
<input
|
|
78
|
-
value={title}
|
|
79
|
-
onChange={(event) => setTitle(event.target.value)}
|
|
80
|
-
placeholder="Article title"
|
|
81
|
-
/>
|
|
82
|
-
|
|
83
|
-
<SlugGenerator
|
|
84
|
-
title={title}
|
|
85
|
-
value={slug}
|
|
86
|
-
onChange={setSlug}
|
|
87
|
-
label="Slug"
|
|
88
|
-
buttonText="Generate Slug"
|
|
89
|
-
/>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## 🌍 Persian Support
|
|
96
|
-
|
|
97
|
-
The component is designed to work with Unicode text, making it suitable for Persian and other non-Latin languages.
|
|
98
|
-
|
|
99
|
-
For example:
|
|
100
|
-
|
|
101
|
-
```text
|
|
102
|
-
آموزش لاراول برای مبتدیان
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
can become:
|
|
106
|
-
|
|
107
|
-
```text
|
|
108
|
-
آموزش-لاراول-برای-مبتدیان
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
English titles are supported as well:
|
|
112
|
-
|
|
113
|
-
```text
|
|
114
|
-
Learn Laravel From Scratch
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
becomes:
|
|
118
|
-
|
|
119
|
-
```text
|
|
120
|
-
learn-laravel-from-scratch
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## ⚙️ API
|
|
124
|
-
|
|
125
|
-
| Prop | Type | Default | Description |
|
|
126
|
-
|---|---|---|---|
|
|
127
|
-
| `title` | `string` | — | Source title used to generate the slug |
|
|
128
|
-
| `value` | `string` | — | Current slug value |
|
|
129
|
-
| `onChange` | `(slug: string) => void` | — | Called when the slug changes |
|
|
130
|
-
| `label` | `string` | `"Slug"` | Input label |
|
|
131
|
-
| `placeholder` | `string` | `"your-slug"` | Input placeholder |
|
|
132
|
-
| `buttonText` | `string` | `"Generate"` | Generate button text |
|
|
133
|
-
| `disabled` | `boolean` | `false` | Disables the component |
|
|
134
|
-
| `className` | `string` | `""` | Custom CSS class |
|
|
135
|
-
| `icon` | `ReactNode` | — | Optional label icon |
|
|
136
|
-
|
|
137
|
-
## 🏗️ Build From Source
|
|
138
|
-
|
|
139
|
-
Clone the repository:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
git clone https://github.com/
|
|
143
|
-
cd slug-generator
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Install dependencies:
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
npm install
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Build the package:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
npm run build
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Development mode:
|
|
159
|
-
|
|
160
|
-
```bash
|
|
161
|
-
npm run dev
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## 📁 Package Architecture
|
|
165
|
-
|
|
166
|
-
```text
|
|
167
|
-
slug-generator/
|
|
168
|
-
├── src/
|
|
169
|
-
│ ├── SlugGenerator.tsx
|
|
170
|
-
│ ├── index.ts
|
|
171
|
-
│ └── types.ts
|
|
172
|
-
├── package.json
|
|
173
|
-
├── tsconfig.json
|
|
174
|
-
├── tsup.config.ts
|
|
175
|
-
└── README.md
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## 🎯 Design Philosophy
|
|
179
|
-
|
|
180
|
-
`@codav/slug-generator` is intentionally focused on one responsibility:
|
|
181
|
-
|
|
182
|
-
> Convert user-provided titles into clean, URL-friendly slugs through a reusable React component.
|
|
183
|
-
|
|
184
|
-
The package is designed to remain independent from any specific CMS, blog system, backend framework, database or application architecture.
|
|
185
|
-
|
|
186
|
-
This makes it suitable for:
|
|
187
|
-
|
|
188
|
-
- Blogs
|
|
189
|
-
- Products
|
|
190
|
-
- Categories
|
|
191
|
-
- Pages
|
|
192
|
-
- Courses
|
|
193
|
-
- Documentation
|
|
194
|
-
- News systems
|
|
195
|
-
- E-commerce platforms
|
|
196
|
-
- Admin dashboards
|
|
197
|
-
- Content management systems
|
|
198
|
-
|
|
199
|
-
## 🛠️ Tech Stack
|
|
200
|
-
|
|
201
|
-
- React
|
|
202
|
-
- TypeScript
|
|
203
|
-
- tsup
|
|
204
|
-
- ESM
|
|
205
|
-
- CommonJS
|
|
206
|
-
|
|
207
|
-
## 📄 License
|
|
208
|
-
|
|
209
|
-
MIT
|
|
210
|
-
|
|
211
|
-
## 👨💻 Author
|
|
212
|
-
|
|
213
|
-
Developed and maintained by **CODAV**.
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
If you find this package useful, consider giving the repository a ⭐ on GitHub.
|
|
1
|
+
# @codav/slug-generator
|
|
2
|
+
|
|
3
|
+
A lightweight, reusable and TypeScript-first React component for generating clean, URL-friendly slugs directly from titles.
|
|
4
|
+
|
|
5
|
+
Built for modern React and Next.js applications with a simple API, zero unnecessary complexity, and easy integration.
|
|
6
|
+
|
|
7
|
+
## ✨ Features
|
|
8
|
+
|
|
9
|
+
- Generate slugs directly from titles
|
|
10
|
+
- Supports Persian, English, numbers and Unicode characters
|
|
11
|
+
- Automatically removes unnecessary characters
|
|
12
|
+
- Converts spaces into `-`
|
|
13
|
+
- Prevents duplicate hyphens
|
|
14
|
+
- Removes leading and trailing hyphens
|
|
15
|
+
- Fully controlled React component
|
|
16
|
+
- Written in TypeScript
|
|
17
|
+
- React 18 and React 19 compatible
|
|
18
|
+
- Next.js compatible
|
|
19
|
+
- Customizable labels and button text
|
|
20
|
+
- Supports custom classes
|
|
21
|
+
- Lightweight package architecture
|
|
22
|
+
- ESM and CommonJS builds
|
|
23
|
+
- Built-in TypeScript declarations
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @codav/slug-generator
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
yarn add @codav/slug-generator
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm add @codav/slug-generator
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 🚀 Basic Usage
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
"use client";
|
|
45
|
+
|
|
46
|
+
import { useState } from "react";
|
|
47
|
+
import { SlugGenerator } from "@codav/slug-generator";
|
|
48
|
+
|
|
49
|
+
export default function Example() {
|
|
50
|
+
const [title, setTitle] = useState("");
|
|
51
|
+
const [slug, setSlug] = useState("");
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<SlugGenerator
|
|
55
|
+
title={title}
|
|
56
|
+
value={slug}
|
|
57
|
+
onChange={setSlug}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 🧩 With a Title Input
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
"use client";
|
|
67
|
+
|
|
68
|
+
import { useState } from "react";
|
|
69
|
+
import { SlugGenerator } from "@codav/slug-generator";
|
|
70
|
+
|
|
71
|
+
export default function BlogForm() {
|
|
72
|
+
const [title, setTitle] = useState("");
|
|
73
|
+
const [slug, setSlug] = useState("");
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<input
|
|
78
|
+
value={title}
|
|
79
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
80
|
+
placeholder="Article title"
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<SlugGenerator
|
|
84
|
+
title={title}
|
|
85
|
+
value={slug}
|
|
86
|
+
onChange={setSlug}
|
|
87
|
+
label="Slug"
|
|
88
|
+
buttonText="Generate Slug"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 🌍 Persian Support
|
|
96
|
+
|
|
97
|
+
The component is designed to work with Unicode text, making it suitable for Persian and other non-Latin languages.
|
|
98
|
+
|
|
99
|
+
For example:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
آموزش لاراول برای مبتدیان
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
can become:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
آموزش-لاراول-برای-مبتدیان
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
English titles are supported as well:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
Learn Laravel From Scratch
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
becomes:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
learn-laravel-from-scratch
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## ⚙️ API
|
|
124
|
+
|
|
125
|
+
| Prop | Type | Default | Description |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| `title` | `string` | — | Source title used to generate the slug |
|
|
128
|
+
| `value` | `string` | — | Current slug value |
|
|
129
|
+
| `onChange` | `(slug: string) => void` | — | Called when the slug changes |
|
|
130
|
+
| `label` | `string` | `"Slug"` | Input label |
|
|
131
|
+
| `placeholder` | `string` | `"your-slug"` | Input placeholder |
|
|
132
|
+
| `buttonText` | `string` | `"Generate"` | Generate button text |
|
|
133
|
+
| `disabled` | `boolean` | `false` | Disables the component |
|
|
134
|
+
| `className` | `string` | `""` | Custom CSS class |
|
|
135
|
+
| `icon` | `ReactNode` | — | Optional label icon |
|
|
136
|
+
|
|
137
|
+
## 🏗️ Build From Source
|
|
138
|
+
|
|
139
|
+
Clone the repository:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git clone https://github.com/MohammadMahdiAhmadi1382/slug-generator.git
|
|
143
|
+
cd slug-generator
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Install dependencies:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm install
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Build the package:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm run build
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Development mode:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm run dev
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 📁 Package Architecture
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
slug-generator/
|
|
168
|
+
├── src/
|
|
169
|
+
│ ├── SlugGenerator.tsx
|
|
170
|
+
│ ├── index.ts
|
|
171
|
+
│ └── types.ts
|
|
172
|
+
├── package.json
|
|
173
|
+
├── tsconfig.json
|
|
174
|
+
├── tsup.config.ts
|
|
175
|
+
└── README.md
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 🎯 Design Philosophy
|
|
179
|
+
|
|
180
|
+
`@codav/slug-generator` is intentionally focused on one responsibility:
|
|
181
|
+
|
|
182
|
+
> Convert user-provided titles into clean, URL-friendly slugs through a reusable React component.
|
|
183
|
+
|
|
184
|
+
The package is designed to remain independent from any specific CMS, blog system, backend framework, database or application architecture.
|
|
185
|
+
|
|
186
|
+
This makes it suitable for:
|
|
187
|
+
|
|
188
|
+
- Blogs
|
|
189
|
+
- Products
|
|
190
|
+
- Categories
|
|
191
|
+
- Pages
|
|
192
|
+
- Courses
|
|
193
|
+
- Documentation
|
|
194
|
+
- News systems
|
|
195
|
+
- E-commerce platforms
|
|
196
|
+
- Admin dashboards
|
|
197
|
+
- Content management systems
|
|
198
|
+
|
|
199
|
+
## 🛠️ Tech Stack
|
|
200
|
+
|
|
201
|
+
- React
|
|
202
|
+
- TypeScript
|
|
203
|
+
- tsup
|
|
204
|
+
- ESM
|
|
205
|
+
- CommonJS
|
|
206
|
+
|
|
207
|
+
## 📄 License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
210
|
+
|
|
211
|
+
## 👨💻 Author
|
|
212
|
+
|
|
213
|
+
Developed and maintained by **CODAV**.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
If you find this package useful, consider giving the repository a ⭐ on GitHub.
|
package/dist/index.d.mts
CHANGED
|
@@ -11,8 +11,11 @@ type SlugGeneratorProps = {
|
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
className?: string;
|
|
13
13
|
icon?: ReactNode;
|
|
14
|
+
dir?: "ltr" | "rtl" | "auto";
|
|
15
|
+
generateOnTitleChange?: boolean;
|
|
16
|
+
transliteratePersian?: boolean;
|
|
14
17
|
};
|
|
15
18
|
|
|
16
|
-
declare function SlugGenerator({ title, value, onChange, label, placeholder, buttonText, disabled, className, icon, }: SlugGeneratorProps): react.JSX.Element;
|
|
19
|
+
declare function SlugGenerator({ title, value, onChange, label, placeholder, buttonText, disabled, className, icon, dir, generateOnTitleChange, transliteratePersian, }: SlugGeneratorProps): react.JSX.Element;
|
|
17
20
|
|
|
18
21
|
export { SlugGenerator, type SlugGeneratorProps };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,8 +11,11 @@ type SlugGeneratorProps = {
|
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
className?: string;
|
|
13
13
|
icon?: ReactNode;
|
|
14
|
+
dir?: "ltr" | "rtl" | "auto";
|
|
15
|
+
generateOnTitleChange?: boolean;
|
|
16
|
+
transliteratePersian?: boolean;
|
|
14
17
|
};
|
|
15
18
|
|
|
16
|
-
declare function SlugGenerator({ title, value, onChange, label, placeholder, buttonText, disabled, className, icon, }: SlugGeneratorProps): react.JSX.Element;
|
|
19
|
+
declare function SlugGenerator({ title, value, onChange, label, placeholder, buttonText, disabled, className, icon, dir, generateOnTitleChange, transliteratePersian, }: SlugGeneratorProps): react.JSX.Element;
|
|
17
20
|
|
|
18
21
|
export { SlugGenerator, type SlugGeneratorProps };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -26,7 +36,60 @@ module.exports = __toCommonJS(index_exports);
|
|
|
26
36
|
|
|
27
37
|
// src/SlugGenerator.tsx
|
|
28
38
|
var import_lucide_react = require("lucide-react");
|
|
39
|
+
var import_clsx = __toESM(require("clsx"));
|
|
29
40
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
41
|
+
var PERSIAN_MAP = {
|
|
42
|
+
\u0627: "a",
|
|
43
|
+
\u0622: "a",
|
|
44
|
+
\u0628: "b",
|
|
45
|
+
\u067E: "p",
|
|
46
|
+
\u062A: "t",
|
|
47
|
+
\u062B: "s",
|
|
48
|
+
\u062C: "j",
|
|
49
|
+
\u0686: "ch",
|
|
50
|
+
\u062D: "h",
|
|
51
|
+
\u062E: "kh",
|
|
52
|
+
\u062F: "d",
|
|
53
|
+
\u0630: "z",
|
|
54
|
+
\u0631: "r",
|
|
55
|
+
\u0632: "z",
|
|
56
|
+
\u0698: "zh",
|
|
57
|
+
\u0633: "s",
|
|
58
|
+
\u0634: "sh",
|
|
59
|
+
\u0635: "s",
|
|
60
|
+
\u0636: "z",
|
|
61
|
+
\u0637: "t",
|
|
62
|
+
\u0638: "z",
|
|
63
|
+
\u0639: "a",
|
|
64
|
+
\u063A: "gh",
|
|
65
|
+
\u0641: "f",
|
|
66
|
+
\u0642: "gh",
|
|
67
|
+
\u06A9: "k",
|
|
68
|
+
\u06AF: "g",
|
|
69
|
+
\u0644: "l",
|
|
70
|
+
\u0645: "m",
|
|
71
|
+
\u0646: "n",
|
|
72
|
+
\u0648: "v",
|
|
73
|
+
\u0647: "h",
|
|
74
|
+
\u06CC: "y",
|
|
75
|
+
\u0626: "y",
|
|
76
|
+
\u0621: "a",
|
|
77
|
+
\u06C0: "h",
|
|
78
|
+
\u0629: "h",
|
|
79
|
+
\u064A: "y",
|
|
80
|
+
\u0643: "k"
|
|
81
|
+
};
|
|
82
|
+
var ARABIC_DIACRITICS = /[\u064B-\u065F\u0670]/g;
|
|
83
|
+
function transliterate(text) {
|
|
84
|
+
return text.normalize("NFKC").replace(ARABIC_DIACRITICS, "").split("").map((char) => PERSIAN_MAP[char] ?? char).join("");
|
|
85
|
+
}
|
|
86
|
+
function generateSlug(title, transliteratePersian) {
|
|
87
|
+
let source = title.normalize("NFKC").replace(ARABIC_DIACRITICS, "");
|
|
88
|
+
if (transliteratePersian) {
|
|
89
|
+
source = transliterate(source);
|
|
90
|
+
}
|
|
91
|
+
return source.toLowerCase().trim().replace(/['’`"]/g, "").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
92
|
+
}
|
|
30
93
|
function SlugGenerator({
|
|
31
94
|
title,
|
|
32
95
|
value,
|
|
@@ -36,15 +99,17 @@ function SlugGenerator({
|
|
|
36
99
|
buttonText = "Generate",
|
|
37
100
|
disabled = false,
|
|
38
101
|
className = "",
|
|
39
|
-
icon
|
|
102
|
+
icon,
|
|
103
|
+
dir = "ltr",
|
|
104
|
+
generateOnTitleChange = false,
|
|
105
|
+
transliteratePersian = true
|
|
40
106
|
}) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
onChange(slug);
|
|
107
|
+
const createSlug = () => {
|
|
108
|
+
onChange(generateSlug(title, transliteratePersian));
|
|
44
109
|
};
|
|
45
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, children: [
|
|
46
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "
|
|
47
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "text-sm font-medium", children: [
|
|
110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: (0, import_clsx.default)("w-full space-y-1.5", className), children: [
|
|
111
|
+
label && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between gap-3", children: [
|
|
112
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "flex items-center gap-2 text-sm font-medium text-(--color-text-primary)", children: [
|
|
48
113
|
icon,
|
|
49
114
|
label
|
|
50
115
|
] }),
|
|
@@ -52,9 +117,16 @@ function SlugGenerator({
|
|
|
52
117
|
"button",
|
|
53
118
|
{
|
|
54
119
|
type: "button",
|
|
55
|
-
onClick:
|
|
120
|
+
onClick: createSlug,
|
|
56
121
|
disabled: disabled || !title.trim(),
|
|
57
|
-
className:
|
|
122
|
+
className: (0, import_clsx.default)(
|
|
123
|
+
"inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-xs font-medium",
|
|
124
|
+
"text-white transition-all duration-200",
|
|
125
|
+
"bg-[var(--color-primary)]",
|
|
126
|
+
"hover:bg-[var(--color-primary-soft)]",
|
|
127
|
+
"focus:outline-none focus:ring-4 focus:ring-[color-mix(in_oklab,var(--color-primary)_15%,transparent)]",
|
|
128
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
129
|
+
),
|
|
58
130
|
children: [
|
|
59
131
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.WandSparkles, { className: "size-4" }),
|
|
60
132
|
buttonText
|
|
@@ -62,17 +134,73 @@ function SlugGenerator({
|
|
|
62
134
|
}
|
|
63
135
|
)
|
|
64
136
|
] }),
|
|
65
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
137
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative group", children: [
|
|
138
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute -inset-[1px] rounded-lg opacity-0 transition group-focus-within:opacity-100" }),
|
|
139
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
140
|
+
"input",
|
|
141
|
+
{
|
|
142
|
+
dir,
|
|
143
|
+
value,
|
|
144
|
+
onChange: (event) => onChange(event.target.value),
|
|
145
|
+
placeholder,
|
|
146
|
+
disabled,
|
|
147
|
+
onKeyDown: (event) => {
|
|
148
|
+
if (event.key === "Enter") {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
createSlug();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
className: (0, import_clsx.default)(
|
|
154
|
+
"relative w-full rounded-lg border border-(--color-border) bg-(--color-bg-main)",
|
|
155
|
+
"px-4 py-3 pe-32 text-sm text-(--color-text-primary)",
|
|
156
|
+
"outline-none transition-all placeholder:text-(--color-text-secondary)/70",
|
|
157
|
+
"hover:border-[color-mix(in_oklab,var(--color-primary)_22%,var(--color-border))]",
|
|
158
|
+
"focus:border-[color-mix(in_oklab,var(--color-primary)_45%,var(--color-border))]",
|
|
159
|
+
"focus:ring-4 focus:ring-[color-mix(in_oklab,var(--color-primary)_12%,transparent)]",
|
|
160
|
+
"dark:bg-white/[0.03]",
|
|
161
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
),
|
|
165
|
+
!label && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
166
|
+
"button",
|
|
167
|
+
{
|
|
168
|
+
type: "button",
|
|
169
|
+
onClick: createSlug,
|
|
170
|
+
disabled: disabled || !title.trim(),
|
|
171
|
+
className: (0, import_clsx.default)(
|
|
172
|
+
"absolute inset-y-1.5 end-1.5 inline-flex items-center gap-1.5",
|
|
173
|
+
"rounded-md px-3 text-xs font-medium text-white",
|
|
174
|
+
"bg-[var(--color-primary)] transition-all duration-200",
|
|
175
|
+
"hover:bg-[var(--color-primary-soft)]",
|
|
176
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
177
|
+
),
|
|
178
|
+
children: [
|
|
179
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.WandSparkles, { className: "size-4" }),
|
|
180
|
+
buttonText
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
),
|
|
184
|
+
label && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
185
|
+
"button",
|
|
186
|
+
{
|
|
187
|
+
type: "button",
|
|
188
|
+
onClick: createSlug,
|
|
189
|
+
disabled: disabled || !title.trim(),
|
|
190
|
+
className: (0, import_clsx.default)(
|
|
191
|
+
"absolute inset-y-1.5 end-1.5 inline-flex items-center gap-1.5",
|
|
192
|
+
"rounded-md px-3 text-xs font-medium text-white",
|
|
193
|
+
"bg-[var(--color-primary)] transition-all duration-200",
|
|
194
|
+
"hover:bg-[var(--color-primary-soft)]",
|
|
195
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
196
|
+
),
|
|
197
|
+
children: [
|
|
198
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.WandSparkles, { className: "size-4" }),
|
|
199
|
+
buttonText
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
] })
|
|
76
204
|
] });
|
|
77
205
|
}
|
|
78
206
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/SlugGenerator.tsx"],"sourcesContent":["export { default as SlugGenerator } from \"./SlugGenerator\";\r\nexport type { SlugGeneratorProps } from \"./types\";","\"use client\";\r\n\r\nimport { WandSparkles } from \"lucide-react\";\r\nimport type { SlugGeneratorProps } from \"./types\";\r\n\r\
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/SlugGenerator.tsx"],"sourcesContent":["export { default as SlugGenerator } from \"./SlugGenerator\";\r\nexport type { SlugGeneratorProps } from \"./types\";","\"use client\";\r\n\r\nimport { WandSparkles } from \"lucide-react\";\r\nimport clsx from \"clsx\";\r\nimport type { SlugGeneratorProps } from \"./types\";\r\n\r\nconst PERSIAN_MAP: Record<string, string> = {\r\n ا: \"a\",\r\n آ: \"a\",\r\n ب: \"b\",\r\n پ: \"p\",\r\n ت: \"t\",\r\n ث: \"s\",\r\n ج: \"j\",\r\n چ: \"ch\",\r\n ح: \"h\",\r\n خ: \"kh\",\r\n د: \"d\",\r\n ذ: \"z\",\r\n ر: \"r\",\r\n ز: \"z\",\r\n ژ: \"zh\",\r\n س: \"s\",\r\n ش: \"sh\",\r\n ص: \"s\",\r\n ض: \"z\",\r\n ط: \"t\",\r\n ظ: \"z\",\r\n ع: \"a\",\r\n غ: \"gh\",\r\n ف: \"f\",\r\n ق: \"gh\",\r\n ک: \"k\",\r\n گ: \"g\",\r\n ل: \"l\",\r\n م: \"m\",\r\n ن: \"n\",\r\n و: \"v\",\r\n ه: \"h\",\r\n ی: \"y\",\r\n ئ: \"y\",\r\n ء: \"a\",\r\n ۀ: \"h\",\r\n ة: \"h\",\r\n ي: \"y\",\r\n ك: \"k\",\r\n};\r\n\r\nconst ARABIC_DIACRITICS = /[\\u064B-\\u065F\\u0670]/g;\r\n\r\nfunction transliterate(text: string) {\r\n return text\r\n .normalize(\"NFKC\")\r\n .replace(ARABIC_DIACRITICS, \"\")\r\n .split(\"\")\r\n .map((char) => PERSIAN_MAP[char] ?? char)\r\n .join(\"\");\r\n}\r\n\r\nfunction generateSlug(\r\n title: string,\r\n transliteratePersian: boolean,\r\n) {\r\n let source = title\r\n .normalize(\"NFKC\")\r\n .replace(ARABIC_DIACRITICS, \"\");\r\n\r\n if (transliteratePersian) {\r\n source = transliterate(source);\r\n }\r\n\r\n return source\r\n .toLowerCase()\r\n .trim()\r\n .replace(/['’`\"]/g, \"\")\r\n .replace(/[^a-z0-9\\s-]/g, \"\")\r\n .replace(/\\s+/g, \"-\")\r\n .replace(/-+/g, \"-\")\r\n .replace(/^-|-$/g, \"\");\r\n}\r\n\r\nexport default function SlugGenerator({\r\n title,\r\n value,\r\n onChange,\r\n label = \"Slug\",\r\n placeholder = \"your-slug\",\r\n buttonText = \"Generate\",\r\n disabled = false,\r\n className = \"\",\r\n icon,\r\n dir = \"ltr\",\r\n generateOnTitleChange = false,\r\n transliteratePersian = true,\r\n}: SlugGeneratorProps) {\r\n const createSlug = () => {\r\n onChange(generateSlug(title, transliteratePersian));\r\n };\r\n\r\n return (\r\n <div className={clsx(\"w-full space-y-1.5\", className)}>\r\n {label && (\r\n <div className=\"flex items-center justify-between gap-3\">\r\n <label className=\"flex items-center gap-2 text-sm font-medium text-(--color-text-primary)\">\r\n {icon}\r\n {label}\r\n </label>\r\n\r\n <button\r\n type=\"button\"\r\n onClick={createSlug}\r\n disabled={disabled || !title.trim()}\r\n className={clsx(\r\n \"inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-xs font-medium\",\r\n \"text-white transition-all duration-200\",\r\n \"bg-[var(--color-primary)]\",\r\n \"hover:bg-[var(--color-primary-soft)]\",\r\n \"focus:outline-none focus:ring-4 focus:ring-[color-mix(in_oklab,var(--color-primary)_15%,transparent)]\",\r\n \"disabled:cursor-not-allowed disabled:opacity-50\",\r\n )}\r\n >\r\n <WandSparkles className=\"size-4\" />\r\n {buttonText}\r\n </button>\r\n </div>\r\n )}\r\n\r\n <div className=\"relative group\">\r\n <div className=\"pointer-events-none absolute -inset-[1px] rounded-lg opacity-0 transition group-focus-within:opacity-100\" />\r\n\r\n <input\r\n dir={dir}\r\n value={value}\r\n onChange={(event) => onChange(event.target.value)}\r\n placeholder={placeholder}\r\n disabled={disabled}\r\n onKeyDown={(event) => {\r\n if (event.key === \"Enter\") {\r\n event.preventDefault();\r\n createSlug();\r\n }\r\n }}\r\n className={clsx(\r\n \"relative w-full rounded-lg border border-(--color-border) bg-(--color-bg-main)\",\r\n \"px-4 py-3 pe-32 text-sm text-(--color-text-primary)\",\r\n \"outline-none transition-all placeholder:text-(--color-text-secondary)/70\",\r\n \"hover:border-[color-mix(in_oklab,var(--color-primary)_22%,var(--color-border))]\",\r\n \"focus:border-[color-mix(in_oklab,var(--color-primary)_45%,var(--color-border))]\",\r\n \"focus:ring-4 focus:ring-[color-mix(in_oklab,var(--color-primary)_12%,transparent)]\",\r\n \"dark:bg-white/[0.03]\",\r\n \"disabled:cursor-not-allowed disabled:opacity-50\",\r\n )}\r\n />\r\n\r\n {!label && (\r\n <button\r\n type=\"button\"\r\n onClick={createSlug}\r\n disabled={disabled || !title.trim()}\r\n className={clsx(\r\n \"absolute inset-y-1.5 end-1.5 inline-flex items-center gap-1.5\",\r\n \"rounded-md px-3 text-xs font-medium text-white\",\r\n \"bg-[var(--color-primary)] transition-all duration-200\",\r\n \"hover:bg-[var(--color-primary-soft)]\",\r\n \"disabled:cursor-not-allowed disabled:opacity-50\",\r\n )}\r\n >\r\n <WandSparkles className=\"size-4\" />\r\n {buttonText}\r\n </button>\r\n )}\r\n\r\n {label && (\r\n <button\r\n type=\"button\"\r\n onClick={createSlug}\r\n disabled={disabled || !title.trim()}\r\n className={clsx(\r\n \"absolute inset-y-1.5 end-1.5 inline-flex items-center gap-1.5\",\r\n \"rounded-md px-3 text-xs font-medium text-white\",\r\n \"bg-[var(--color-primary)] transition-all duration-200\",\r\n \"hover:bg-[var(--color-primary-soft)]\",\r\n \"disabled:cursor-not-allowed disabled:opacity-50\",\r\n )}\r\n >\r\n <WandSparkles className=\"size-4\" />\r\n {buttonText}\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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","names":["clsx"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
// src/SlugGenerator.tsx
|
|
2
2
|
import { WandSparkles } from "lucide-react";
|
|
3
|
+
import clsx from "clsx";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
var PERSIAN_MAP = {
|
|
6
|
+
\u0627: "a",
|
|
7
|
+
\u0622: "a",
|
|
8
|
+
\u0628: "b",
|
|
9
|
+
\u067E: "p",
|
|
10
|
+
\u062A: "t",
|
|
11
|
+
\u062B: "s",
|
|
12
|
+
\u062C: "j",
|
|
13
|
+
\u0686: "ch",
|
|
14
|
+
\u062D: "h",
|
|
15
|
+
\u062E: "kh",
|
|
16
|
+
\u062F: "d",
|
|
17
|
+
\u0630: "z",
|
|
18
|
+
\u0631: "r",
|
|
19
|
+
\u0632: "z",
|
|
20
|
+
\u0698: "zh",
|
|
21
|
+
\u0633: "s",
|
|
22
|
+
\u0634: "sh",
|
|
23
|
+
\u0635: "s",
|
|
24
|
+
\u0636: "z",
|
|
25
|
+
\u0637: "t",
|
|
26
|
+
\u0638: "z",
|
|
27
|
+
\u0639: "a",
|
|
28
|
+
\u063A: "gh",
|
|
29
|
+
\u0641: "f",
|
|
30
|
+
\u0642: "gh",
|
|
31
|
+
\u06A9: "k",
|
|
32
|
+
\u06AF: "g",
|
|
33
|
+
\u0644: "l",
|
|
34
|
+
\u0645: "m",
|
|
35
|
+
\u0646: "n",
|
|
36
|
+
\u0648: "v",
|
|
37
|
+
\u0647: "h",
|
|
38
|
+
\u06CC: "y",
|
|
39
|
+
\u0626: "y",
|
|
40
|
+
\u0621: "a",
|
|
41
|
+
\u06C0: "h",
|
|
42
|
+
\u0629: "h",
|
|
43
|
+
\u064A: "y",
|
|
44
|
+
\u0643: "k"
|
|
45
|
+
};
|
|
46
|
+
var ARABIC_DIACRITICS = /[\u064B-\u065F\u0670]/g;
|
|
47
|
+
function transliterate(text) {
|
|
48
|
+
return text.normalize("NFKC").replace(ARABIC_DIACRITICS, "").split("").map((char) => PERSIAN_MAP[char] ?? char).join("");
|
|
49
|
+
}
|
|
50
|
+
function generateSlug(title, transliteratePersian) {
|
|
51
|
+
let source = title.normalize("NFKC").replace(ARABIC_DIACRITICS, "");
|
|
52
|
+
if (transliteratePersian) {
|
|
53
|
+
source = transliterate(source);
|
|
54
|
+
}
|
|
55
|
+
return source.toLowerCase().trim().replace(/['’`"]/g, "").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
56
|
+
}
|
|
4
57
|
function SlugGenerator({
|
|
5
58
|
title,
|
|
6
59
|
value,
|
|
@@ -10,15 +63,17 @@ function SlugGenerator({
|
|
|
10
63
|
buttonText = "Generate",
|
|
11
64
|
disabled = false,
|
|
12
65
|
className = "",
|
|
13
|
-
icon
|
|
66
|
+
icon,
|
|
67
|
+
dir = "ltr",
|
|
68
|
+
generateOnTitleChange = false,
|
|
69
|
+
transliteratePersian = true
|
|
14
70
|
}) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
onChange(slug);
|
|
71
|
+
const createSlug = () => {
|
|
72
|
+
onChange(generateSlug(title, transliteratePersian));
|
|
18
73
|
};
|
|
19
|
-
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
20
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
21
|
-
/* @__PURE__ */ jsxs("label", { className: "text-sm font-medium", children: [
|
|
74
|
+
return /* @__PURE__ */ jsxs("div", { className: clsx("w-full space-y-1.5", className), children: [
|
|
75
|
+
label && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
76
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm font-medium text-(--color-text-primary)", children: [
|
|
22
77
|
icon,
|
|
23
78
|
label
|
|
24
79
|
] }),
|
|
@@ -26,9 +81,16 @@ function SlugGenerator({
|
|
|
26
81
|
"button",
|
|
27
82
|
{
|
|
28
83
|
type: "button",
|
|
29
|
-
onClick:
|
|
84
|
+
onClick: createSlug,
|
|
30
85
|
disabled: disabled || !title.trim(),
|
|
31
|
-
className:
|
|
86
|
+
className: clsx(
|
|
87
|
+
"inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-xs font-medium",
|
|
88
|
+
"text-white transition-all duration-200",
|
|
89
|
+
"bg-[var(--color-primary)]",
|
|
90
|
+
"hover:bg-[var(--color-primary-soft)]",
|
|
91
|
+
"focus:outline-none focus:ring-4 focus:ring-[color-mix(in_oklab,var(--color-primary)_15%,transparent)]",
|
|
92
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
93
|
+
),
|
|
32
94
|
children: [
|
|
33
95
|
/* @__PURE__ */ jsx(WandSparkles, { className: "size-4" }),
|
|
34
96
|
buttonText
|
|
@@ -36,17 +98,73 @@ function SlugGenerator({
|
|
|
36
98
|
}
|
|
37
99
|
)
|
|
38
100
|
] }),
|
|
39
|
-
/* @__PURE__ */
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
101
|
+
/* @__PURE__ */ jsxs("div", { className: "relative group", children: [
|
|
102
|
+
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute -inset-[1px] rounded-lg opacity-0 transition group-focus-within:opacity-100" }),
|
|
103
|
+
/* @__PURE__ */ jsx(
|
|
104
|
+
"input",
|
|
105
|
+
{
|
|
106
|
+
dir,
|
|
107
|
+
value,
|
|
108
|
+
onChange: (event) => onChange(event.target.value),
|
|
109
|
+
placeholder,
|
|
110
|
+
disabled,
|
|
111
|
+
onKeyDown: (event) => {
|
|
112
|
+
if (event.key === "Enter") {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
createSlug();
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
className: clsx(
|
|
118
|
+
"relative w-full rounded-lg border border-(--color-border) bg-(--color-bg-main)",
|
|
119
|
+
"px-4 py-3 pe-32 text-sm text-(--color-text-primary)",
|
|
120
|
+
"outline-none transition-all placeholder:text-(--color-text-secondary)/70",
|
|
121
|
+
"hover:border-[color-mix(in_oklab,var(--color-primary)_22%,var(--color-border))]",
|
|
122
|
+
"focus:border-[color-mix(in_oklab,var(--color-primary)_45%,var(--color-border))]",
|
|
123
|
+
"focus:ring-4 focus:ring-[color-mix(in_oklab,var(--color-primary)_12%,transparent)]",
|
|
124
|
+
"dark:bg-white/[0.03]",
|
|
125
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
),
|
|
129
|
+
!label && /* @__PURE__ */ jsxs(
|
|
130
|
+
"button",
|
|
131
|
+
{
|
|
132
|
+
type: "button",
|
|
133
|
+
onClick: createSlug,
|
|
134
|
+
disabled: disabled || !title.trim(),
|
|
135
|
+
className: clsx(
|
|
136
|
+
"absolute inset-y-1.5 end-1.5 inline-flex items-center gap-1.5",
|
|
137
|
+
"rounded-md px-3 text-xs font-medium text-white",
|
|
138
|
+
"bg-[var(--color-primary)] transition-all duration-200",
|
|
139
|
+
"hover:bg-[var(--color-primary-soft)]",
|
|
140
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
141
|
+
),
|
|
142
|
+
children: [
|
|
143
|
+
/* @__PURE__ */ jsx(WandSparkles, { className: "size-4" }),
|
|
144
|
+
buttonText
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
label && /* @__PURE__ */ jsxs(
|
|
149
|
+
"button",
|
|
150
|
+
{
|
|
151
|
+
type: "button",
|
|
152
|
+
onClick: createSlug,
|
|
153
|
+
disabled: disabled || !title.trim(),
|
|
154
|
+
className: clsx(
|
|
155
|
+
"absolute inset-y-1.5 end-1.5 inline-flex items-center gap-1.5",
|
|
156
|
+
"rounded-md px-3 text-xs font-medium text-white",
|
|
157
|
+
"bg-[var(--color-primary)] transition-all duration-200",
|
|
158
|
+
"hover:bg-[var(--color-primary-soft)]",
|
|
159
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
160
|
+
),
|
|
161
|
+
children: [
|
|
162
|
+
/* @__PURE__ */ jsx(WandSparkles, { className: "size-4" }),
|
|
163
|
+
buttonText
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
] })
|
|
50
168
|
] });
|
|
51
169
|
}
|
|
52
170
|
export {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/SlugGenerator.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport { WandSparkles } from \"lucide-react\";\r\nimport type { SlugGeneratorProps } from \"./types\";\r\n\r\
|
|
1
|
+
{"version":3,"sources":["../src/SlugGenerator.tsx"],"sourcesContent":["\"use client\";\r\n\r\nimport { WandSparkles } from \"lucide-react\";\r\nimport clsx from \"clsx\";\r\nimport type { SlugGeneratorProps } from \"./types\";\r\n\r\nconst PERSIAN_MAP: Record<string, string> = {\r\n ا: \"a\",\r\n آ: \"a\",\r\n ب: \"b\",\r\n پ: \"p\",\r\n ت: \"t\",\r\n ث: \"s\",\r\n ج: \"j\",\r\n چ: \"ch\",\r\n ح: \"h\",\r\n خ: \"kh\",\r\n د: \"d\",\r\n ذ: \"z\",\r\n ر: \"r\",\r\n ز: \"z\",\r\n ژ: \"zh\",\r\n س: \"s\",\r\n ش: \"sh\",\r\n ص: \"s\",\r\n ض: \"z\",\r\n ط: \"t\",\r\n ظ: \"z\",\r\n ع: \"a\",\r\n غ: \"gh\",\r\n ف: \"f\",\r\n ق: \"gh\",\r\n ک: \"k\",\r\n گ: \"g\",\r\n ل: \"l\",\r\n م: \"m\",\r\n ن: \"n\",\r\n و: \"v\",\r\n ه: \"h\",\r\n ی: \"y\",\r\n ئ: \"y\",\r\n ء: \"a\",\r\n ۀ: \"h\",\r\n ة: \"h\",\r\n ي: \"y\",\r\n ك: \"k\",\r\n};\r\n\r\nconst ARABIC_DIACRITICS = /[\\u064B-\\u065F\\u0670]/g;\r\n\r\nfunction transliterate(text: string) {\r\n return text\r\n .normalize(\"NFKC\")\r\n .replace(ARABIC_DIACRITICS, \"\")\r\n .split(\"\")\r\n .map((char) => PERSIAN_MAP[char] ?? char)\r\n .join(\"\");\r\n}\r\n\r\nfunction generateSlug(\r\n title: string,\r\n transliteratePersian: boolean,\r\n) {\r\n let source = title\r\n .normalize(\"NFKC\")\r\n .replace(ARABIC_DIACRITICS, \"\");\r\n\r\n if (transliteratePersian) {\r\n source = transliterate(source);\r\n }\r\n\r\n return source\r\n .toLowerCase()\r\n .trim()\r\n .replace(/['’`\"]/g, \"\")\r\n .replace(/[^a-z0-9\\s-]/g, \"\")\r\n .replace(/\\s+/g, \"-\")\r\n .replace(/-+/g, \"-\")\r\n .replace(/^-|-$/g, \"\");\r\n}\r\n\r\nexport default function SlugGenerator({\r\n title,\r\n value,\r\n onChange,\r\n label = \"Slug\",\r\n placeholder = \"your-slug\",\r\n buttonText = \"Generate\",\r\n disabled = false,\r\n className = \"\",\r\n icon,\r\n dir = \"ltr\",\r\n generateOnTitleChange = false,\r\n transliteratePersian = true,\r\n}: SlugGeneratorProps) {\r\n const createSlug = () => {\r\n onChange(generateSlug(title, transliteratePersian));\r\n };\r\n\r\n return (\r\n <div className={clsx(\"w-full space-y-1.5\", className)}>\r\n {label && (\r\n <div className=\"flex items-center justify-between gap-3\">\r\n <label className=\"flex items-center gap-2 text-sm font-medium text-(--color-text-primary)\">\r\n {icon}\r\n {label}\r\n </label>\r\n\r\n <button\r\n type=\"button\"\r\n onClick={createSlug}\r\n disabled={disabled || !title.trim()}\r\n className={clsx(\r\n \"inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-xs font-medium\",\r\n \"text-white transition-all duration-200\",\r\n \"bg-[var(--color-primary)]\",\r\n \"hover:bg-[var(--color-primary-soft)]\",\r\n \"focus:outline-none focus:ring-4 focus:ring-[color-mix(in_oklab,var(--color-primary)_15%,transparent)]\",\r\n \"disabled:cursor-not-allowed disabled:opacity-50\",\r\n )}\r\n >\r\n <WandSparkles className=\"size-4\" />\r\n {buttonText}\r\n </button>\r\n </div>\r\n )}\r\n\r\n <div className=\"relative group\">\r\n <div className=\"pointer-events-none absolute -inset-[1px] rounded-lg opacity-0 transition group-focus-within:opacity-100\" />\r\n\r\n <input\r\n dir={dir}\r\n value={value}\r\n onChange={(event) => onChange(event.target.value)}\r\n placeholder={placeholder}\r\n disabled={disabled}\r\n onKeyDown={(event) => {\r\n if (event.key === \"Enter\") {\r\n event.preventDefault();\r\n createSlug();\r\n }\r\n }}\r\n className={clsx(\r\n \"relative w-full rounded-lg border border-(--color-border) bg-(--color-bg-main)\",\r\n \"px-4 py-3 pe-32 text-sm text-(--color-text-primary)\",\r\n \"outline-none transition-all placeholder:text-(--color-text-secondary)/70\",\r\n \"hover:border-[color-mix(in_oklab,var(--color-primary)_22%,var(--color-border))]\",\r\n \"focus:border-[color-mix(in_oklab,var(--color-primary)_45%,var(--color-border))]\",\r\n \"focus:ring-4 focus:ring-[color-mix(in_oklab,var(--color-primary)_12%,transparent)]\",\r\n \"dark:bg-white/[0.03]\",\r\n \"disabled:cursor-not-allowed disabled:opacity-50\",\r\n )}\r\n />\r\n\r\n {!label && (\r\n <button\r\n type=\"button\"\r\n onClick={createSlug}\r\n disabled={disabled || !title.trim()}\r\n className={clsx(\r\n \"absolute inset-y-1.5 end-1.5 inline-flex items-center gap-1.5\",\r\n \"rounded-md px-3 text-xs font-medium text-white\",\r\n \"bg-[var(--color-primary)] transition-all duration-200\",\r\n \"hover:bg-[var(--color-primary-soft)]\",\r\n \"disabled:cursor-not-allowed disabled:opacity-50\",\r\n )}\r\n >\r\n <WandSparkles className=\"size-4\" />\r\n {buttonText}\r\n </button>\r\n )}\r\n\r\n {label && (\r\n <button\r\n type=\"button\"\r\n onClick={createSlug}\r\n disabled={disabled || !title.trim()}\r\n className={clsx(\r\n \"absolute inset-y-1.5 end-1.5 inline-flex items-center gap-1.5\",\r\n \"rounded-md px-3 text-xs font-medium text-white\",\r\n \"bg-[var(--color-primary)] transition-all duration-200\",\r\n \"hover:bg-[var(--color-primary-soft)]\",\r\n \"disabled:cursor-not-allowed disabled:opacity-50\",\r\n )}\r\n >\r\n <WandSparkles className=\"size-4\" />\r\n {buttonText}\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codav/slug-generator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A lightweight, reusable TypeScript-first React slug generator component",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
"dev": "tsup --watch"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
+
"clsx": "^2.1.1",
|
|
17
|
+
"lucide-react": "^0.400.0 || ^1.0.0",
|
|
16
18
|
"react": "^18.0.0 || ^19.0.0",
|
|
17
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
18
|
-
"lucide-react": "^0.400.0"
|
|
19
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@types/react": "^19.0.0",
|
|
@@ -25,4 +26,4 @@
|
|
|
25
26
|
"typescript": "^5.0.0"
|
|
26
27
|
},
|
|
27
28
|
"license": "MIT"
|
|
28
|
-
}
|
|
29
|
+
}
|