tsukikage 0.1.0
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 +160 -0
- package/dist/components/Ranking.d.ts +2 -0
- package/dist/components/RankingItem.d.ts +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/main.d.ts +1 -0
- package/dist/tsukikage.css +1 -0
- package/dist/tsukikage.js +85 -0
- package/dist/tsukikage.js.map +1 -0
- package/dist/tsukikage.umd.cjs +2 -0
- package/dist/tsukikage.umd.cjs.map +1 -0
- package/dist/types/ranking.types.d.ts +20 -0
- package/dist/utils/ranking.utils.d.ts +8 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# 🏆 tsukikage
|
|
2
|
+
|
|
3
|
+
> **月影** — *moonlight* in Japanese. A sleek, fully customizable React ranking component library.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://react.dev/)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✨ What is tsukikage?
|
|
13
|
+
|
|
14
|
+
**tsukikage** is a React component library for building beautiful, flexible ranking lists. Drop it into your project, pass your data, and get a fully styled, animated leaderboard — with total control over how it looks and behaves.
|
|
15
|
+
|
|
16
|
+
Whether you're building a game leaderboard, a points-based system, a productivity tracker, or any competitive UI, tsukikage gives you the tools to make it shine. 🌙
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🚀 Getting Started
|
|
21
|
+
|
|
22
|
+
> 🚧 **This library is currently in active development.** The API may change before the first stable release.
|
|
23
|
+
|
|
24
|
+
### Installation *(coming soon)*
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install tsukikage
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Usage
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Ranking } from 'tsukikage'
|
|
34
|
+
|
|
35
|
+
const entries = [
|
|
36
|
+
{ id: 1, label: "Hector", score: 9840 },
|
|
37
|
+
{ id: 2, label: "Alex Poatan", score: 7200 },
|
|
38
|
+
{ id: 3, label: "Jon Jones", score: 6800 },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
function App() {
|
|
42
|
+
return (
|
|
43
|
+
<Ranking
|
|
44
|
+
entries={entries}
|
|
45
|
+
title="Leaderboard"
|
|
46
|
+
scoreType={1}
|
|
47
|
+
highlightFn={(entry) => entry.score > 9000}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🧩 Components
|
|
56
|
+
|
|
57
|
+
### `<Ranking />`
|
|
58
|
+
|
|
59
|
+
The main component. Accepts a list of entries and renders a sorted, styled ranking list.
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Description |
|
|
62
|
+
|---------------|-----------------------------------------------|---------|-----------------------------------------------------------------|
|
|
63
|
+
| `entries` | `RankingEntry[]` | — | **Required.** Array of items to rank. |
|
|
64
|
+
| `title` | `string` | — | Optional heading displayed above the list. |
|
|
65
|
+
| `scoreType` | `1 \| 2 \| 3` | `1` | Controls the score unit label: `pts`, `xp`, or `coins`. |
|
|
66
|
+
| `highlightFn` | `(entry, index) => boolean` | — | Function that determines which entries get a highlight effect. |
|
|
67
|
+
|
|
68
|
+
### `RankingEntry` type
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
interface RankingEntry {
|
|
72
|
+
id: string | number // Unique identifier
|
|
73
|
+
label: string // Display name
|
|
74
|
+
score: number // Numeric score (used for sorting)
|
|
75
|
+
avatar?: string // Optional image URL
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🎨 Visual Features
|
|
82
|
+
|
|
83
|
+
- 🥇🥈🥉 **Medal badges** for the top 3 positions
|
|
84
|
+
- ✨ **Highlight animations** — shine effect, scale, and glow for featured entries
|
|
85
|
+
- 🖼️ **Avatar support** — image or auto-generated initials fallback
|
|
86
|
+
- 🏷️ **Score labels** — pts / xp / coins (more coming soon)
|
|
87
|
+
- 📐 **Smooth hover interactions** — lift, scale, shadow transitions
|
|
88
|
+
- 🎞️ **CSS-only animations** — no JavaScript animation dependencies
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 🛣️ Roadmap
|
|
93
|
+
|
|
94
|
+
Here's what's planned for tsukikage:
|
|
95
|
+
|
|
96
|
+
- [ ] 🎭 Multiple visual themes (dark, neon, minimal, glass, etc.)
|
|
97
|
+
- [ ] 🔦 More highlight types (pulse, border glow, badge, crown)
|
|
98
|
+
- [ ] 🔢 Custom score formatters
|
|
99
|
+
- [ ] 🏷️ Custom score unit labels (beyond `pts`, `xp`, `coins`)
|
|
100
|
+
- [ ] 🔄 Animated re-ranking (score changes with smooth transitions)
|
|
101
|
+
- [ ] 📱 Mobile-first responsive variants
|
|
102
|
+
- [ ] ♿ Full accessibility (ARIA roles, keyboard navigation)
|
|
103
|
+
- [ ] 🌐 npm package release
|
|
104
|
+
- [ ] 📖 Storybook documentation site
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 🗂️ Project Structure
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
src/
|
|
112
|
+
├── components/
|
|
113
|
+
│ ├── Ranking.tsx # Main ranking component
|
|
114
|
+
│ └── RankingItem.tsx # Individual row component
|
|
115
|
+
├── css/
|
|
116
|
+
│ └── Ranking.module.css # Scoped styles with animations
|
|
117
|
+
├── types/
|
|
118
|
+
│ └── ranking.types.ts # TypeScript interfaces & types
|
|
119
|
+
├── utils/
|
|
120
|
+
│ └── ranking.utils.ts # Sorting helpers & medal logic
|
|
121
|
+
└── main.tsx # App entry point
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 🛠️ Development
|
|
127
|
+
|
|
128
|
+
This project uses **Vite** + **React 19** + **TypeScript**.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Install dependencies
|
|
132
|
+
npm install
|
|
133
|
+
|
|
134
|
+
# Start dev server
|
|
135
|
+
npm run dev
|
|
136
|
+
|
|
137
|
+
# Build
|
|
138
|
+
npm run build
|
|
139
|
+
|
|
140
|
+
# Lint
|
|
141
|
+
npm run lint
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 🤝 Contributing
|
|
147
|
+
|
|
148
|
+
Contributions, ideas, and feedback are very welcome! This is an early-stage project and the API is still being shaped. Feel free to open an issue or submit a PR.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 📄 License
|
|
153
|
+
|
|
154
|
+
MIT © tsukikage contributors
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
<p align="center">
|
|
159
|
+
Made with 🌙 and <strong>React</strong>
|
|
160
|
+
</p>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Ranking } from './components/Ranking';
|
|
2
|
+
export { RankingItem } from './components/RankingItem';
|
|
3
|
+
export type { RankingEntry, RankingProps, RankingItemProps, ScoreType, } from './types/ranking.types';
|
|
4
|
+
export { getMedal, sortByScore, createSorter } from './utils/ranking.utils';
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
._wrapper_1nrlo_1{background:linear-gradient(135deg,#f5f3ff,#ede9fe)}._container_1nrlo_5{width:100%;max-width:420px;padding:20px;border-radius:16px;background:#fff;box-shadow:0 10px 30px #7c3aed26;display:flex;flex-direction:column}._title_1nrlo_16{font-size:1.3rem;font-weight:700;margin-bottom:18px;text-align:center;color:#5b21b6;position:relative}._title_1nrlo_16:after{content:"";display:block;width:40px;height:3px;background:#7c3aed;margin:8px auto 0;border-radius:999px}._list_1nrlo_35{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}._item_1nrlo_44{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:12px;background:#fafafa;transition:all .2s ease;cursor:pointer;position:relative}._item_1nrlo_44:hover{transform:translateY(-4px) scale(1.01);background:#f3f0ff;box-shadow:0 8px 20px #7c3aed33;z-index:2}._item_1nrlo_44:nth-child(1){background:linear-gradient(135deg,#ede9fe,#ddd6fe)}._item_1nrlo_44:nth-child(2){background:linear-gradient(135deg,#f5f3ff,#ede9fe)}._item_1nrlo_44:nth-child(3){background:linear-gradient(135deg,#faf5ff,#f3e8ff)}._position_1nrlo_75{min-width:28px;font-size:.9rem;font-weight:700;color:#7c3aed;text-align:center}._avatar_1nrlo_83{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#7c3aed,#5b21b6);color:#fff;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0;overflow:hidden;box-shadow:0 4px 10px #7c3aed4d}._avatar_1nrlo_83 img{width:100%;height:100%;object-fit:cover}._label_1nrlo_105{flex:1;font-size:.95rem;font-weight:500;color:#2e1065}._score_1nrlo_112{font-size:.9rem;font-weight:700;color:#7c3aed}._scoreUnit_1nrlo_118{font-size:.7rem;font-weight:400;opacity:.6;margin-left:3px}._highlight_1nrlo_125{position:relative;background:linear-gradient(135deg,#ede9fe,#ddd6fe);transform:scale(1.02);z-index:1;transition:all .25s ease}._highlight_1nrlo_125:before{content:"";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(120deg,transparent,rgba(255,255,255,.35),transparent);opacity:0;transform:translate(-100%);animation:_shine_1nrlo_1 3s infinite}._highlight_1nrlo_125:after{content:"";position:absolute;inset:-2px;border-radius:inherit;background:linear-gradient(135deg,#7c3aed,transparent);opacity:.15;z-index:-1;transition:opacity .3s ease}._highlight_1nrlo_125:hover{transform:scale(1.06) translateY(-6px);box-shadow:0 12px 30px #7c3aed59}._highlight_1nrlo_125:hover:before{animation-duration:1.2s;opacity:1}._highlight_1nrlo_125:hover:after{opacity:.35}._highlight_1nrlo_125:hover ._avatar_1nrlo_83{transform:scale(1.1)}@keyframes _shine_1nrlo_1{0%{opacity:0;transform:translate(-100%)}20%{opacity:1}60%{transform:translate(100%);opacity:0}to{opacity:0}}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsxs as p, jsx as e } from "react/jsx-runtime";
|
|
2
|
+
const d = {
|
|
3
|
+
1: "🥇",
|
|
4
|
+
2: "🥈",
|
|
5
|
+
3: "🥉"
|
|
6
|
+
}, g = (s) => d[s] ?? null, u = (s, t) => {
|
|
7
|
+
const { secondaryKey: o } = t || {};
|
|
8
|
+
return [...s].sort((r, l) => {
|
|
9
|
+
const a = l.score - r.score;
|
|
10
|
+
if (a !== 0) return a;
|
|
11
|
+
if (o) {
|
|
12
|
+
const c = r[o], i = l[o];
|
|
13
|
+
if (c != null && i != null)
|
|
14
|
+
return String(c).localeCompare(String(i));
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
});
|
|
18
|
+
}, k = (s) => (t) => [...t].sort(s), N = "_wrapper_1nrlo_1", b = "_container_1nrlo_5", f = "_title_1nrlo_16", v = "_list_1nrlo_35", S = "_item_1nrlo_44", U = "_position_1nrlo_75", L = "_avatar_1nrlo_83", w = "_label_1nrlo_105", E = "_score_1nrlo_112", x = "_scoreUnit_1nrlo_118", C = "_highlight_1nrlo_125", n = {
|
|
19
|
+
wrapper: N,
|
|
20
|
+
container: b,
|
|
21
|
+
title: f,
|
|
22
|
+
list: v,
|
|
23
|
+
item: S,
|
|
24
|
+
position: U,
|
|
25
|
+
avatar: L,
|
|
26
|
+
label: w,
|
|
27
|
+
score: E,
|
|
28
|
+
scoreUnit: x,
|
|
29
|
+
highlight: C
|
|
30
|
+
};
|
|
31
|
+
function R({
|
|
32
|
+
entry: s,
|
|
33
|
+
position: t,
|
|
34
|
+
scoreLabel: o,
|
|
35
|
+
isHighlighted: r
|
|
36
|
+
}) {
|
|
37
|
+
const l = g(t), a = s.label.slice(0, 2).toUpperCase();
|
|
38
|
+
return /* @__PURE__ */ p("li", { className: `${n.item} ${r ? n.highlight : ""}`, children: [
|
|
39
|
+
/* @__PURE__ */ e("span", { className: n.position, children: l ?? `#${t}` }),
|
|
40
|
+
/* @__PURE__ */ e("span", { className: n.avatar, children: s.avatar ? /* @__PURE__ */ e("img", { src: s.avatar, alt: s.label }) : a }),
|
|
41
|
+
/* @__PURE__ */ e("span", { className: n.label, children: s.label }),
|
|
42
|
+
/* @__PURE__ */ p("span", { className: n.score, children: [
|
|
43
|
+
s.score.toLocaleString("en-US"),
|
|
44
|
+
/* @__PURE__ */ e("span", { className: n.scoreUnit, children: o })
|
|
45
|
+
] })
|
|
46
|
+
] });
|
|
47
|
+
}
|
|
48
|
+
const $ = {
|
|
49
|
+
1: "pts",
|
|
50
|
+
2: "xp",
|
|
51
|
+
3: "coins"
|
|
52
|
+
};
|
|
53
|
+
function A({
|
|
54
|
+
entries: s,
|
|
55
|
+
title: t,
|
|
56
|
+
scoreType: o = 1,
|
|
57
|
+
highlightFn: r,
|
|
58
|
+
limit: l
|
|
59
|
+
}) {
|
|
60
|
+
const a = u(s), c = l ? a.slice(0, l) : a, i = $[o];
|
|
61
|
+
return /* @__PURE__ */ e("div", { className: n.wrapper, children: /* @__PURE__ */ p("div", { className: n.container, children: [
|
|
62
|
+
t && /* @__PURE__ */ e("h2", { className: n.title, children: t }),
|
|
63
|
+
/* @__PURE__ */ e("ol", { className: n.list, children: c.map((_, h) => {
|
|
64
|
+
const m = r ? r(_, h) : !1;
|
|
65
|
+
return /* @__PURE__ */ e(
|
|
66
|
+
R,
|
|
67
|
+
{
|
|
68
|
+
entry: _,
|
|
69
|
+
position: h + 1,
|
|
70
|
+
scoreLabel: i,
|
|
71
|
+
isHighlighted: m
|
|
72
|
+
},
|
|
73
|
+
_.id
|
|
74
|
+
);
|
|
75
|
+
}) })
|
|
76
|
+
] }) });
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
A as Ranking,
|
|
80
|
+
R as RankingItem,
|
|
81
|
+
k as createSorter,
|
|
82
|
+
g as getMedal,
|
|
83
|
+
u as sortByScore
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=tsukikage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsukikage.js","sources":["../src/utils/ranking.utils.ts","../src/components/RankingItem.tsx","../src/components/Ranking.tsx"],"sourcesContent":["import type { RankingEntry } from \"../types/ranking.types\";\n\nconst MEDALS: Record<number, string> = {\n 1: \"🥇\",\n 2: \"🥈\",\n 3: \"🥉\",\n};\n\nexport const getMedal = (position: number): string | null => {\n return MEDALS[position] ?? null;\n};\n\ntype SortOptions = {\n secondaryKey?: keyof RankingEntry;\n};\n\nexport const sortByScore = (\n entries: RankingEntry[],\n options?: SortOptions\n): RankingEntry[] => {\n const { secondaryKey } = options || {};\n\n return [...entries].sort((a, b) => {\n const scoreDiff = b.score - a.score;\n\n if (scoreDiff !== 0) return scoreDiff;\n\n if (secondaryKey) {\n const aValue = a[secondaryKey];\n const bValue = b[secondaryKey];\n\n if (aValue != null && bValue != null) {\n return String(aValue).localeCompare(String(bValue));\n }\n }\n\n return 0;\n });\n};\n\nexport const createSorter =\n <T>(compareFn: (a: T, b: T) => number) =>\n (list: T[]): T[] =>\n [...list].sort(compareFn);","import type { RankingItemProps } from \"../types/ranking.types\";\nimport { getMedal } from \"../utils/ranking.utils\";\nimport styles from \"../css/Ranking.module.css\";\n\nexport function RankingItem({\n entry,\n position,\n scoreLabel,\n isHighlighted,\n}: RankingItemProps) {\n const medal = getMedal(position);\n const initials = entry.label.slice(0, 2).toUpperCase();\n\n return (\n <li className={`${styles.item} ${isHighlighted ? styles.highlight : \"\"}`}>\n <span className={styles.position}>\n {medal ?? `#${position}`}\n </span>\n\n <span className={styles.avatar}>\n {entry.avatar ? (\n <img src={entry.avatar} alt={entry.label} />\n ) : (\n initials\n )}\n </span>\n\n <span className={styles.label}>{entry.label}</span>\n\n <span className={styles.score}>\n {entry.score.toLocaleString(\"en-US\")}\n <span className={styles.scoreUnit}>{scoreLabel}</span>\n </span>\n </li>\n );\n}","import type { RankingProps, ScoreType } from \"../types/ranking.types\";\nimport { sortByScore } from \"../utils/ranking.utils\";\n// @ts-ignore: CSS module without type declarations\nimport styles from \"../css/Ranking.module.css\";\nimport { RankingItem } from \"./RankingItem\";\n\nconst SCORE_LABELS: Record<ScoreType, string> = {\n 1: \"pts\",\n 2: \"xp\",\n 3: \"coins\",\n};\n\nexport function Ranking({\n entries,\n title,\n scoreType = 1,\n highlightFn,\n limit,\n}: RankingProps) {\n const sorted = sortByScore(entries);\n\n const visibleEntries = limit\n ? sorted.slice(0, limit)\n : sorted;\n\n const scoreLabel = SCORE_LABELS[scoreType];\n\n return (\n <div className={styles.wrapper}>\n <div className={styles.container}>\n {title && <h2 className={styles.title}>{title}</h2>}\n\n <ol className={styles.list}>\n {visibleEntries.map((entry, index) => {\n const isHighlighted = highlightFn\n ? highlightFn(entry, index)\n : false;\n\n return (\n <RankingItem\n key={entry.id}\n entry={entry}\n position={index + 1}\n scoreLabel={scoreLabel}\n isHighlighted={isHighlighted}\n />\n );\n })}\n </ol>\n </div>\n </div>\n );\n}"],"names":["MEDALS","getMedal","position","sortByScore","entries","options","secondaryKey","a","b","scoreDiff","aValue","bValue","createSorter","compareFn","list","RankingItem","entry","scoreLabel","isHighlighted","medal","initials","jsxs","styles","jsx","SCORE_LABELS","Ranking","title","scoreType","highlightFn","limit","sorted","visibleEntries","index"],"mappings":";AAEA,MAAMA,IAAiC;AAAA,EACrC,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL,GAEaC,IAAW,CAACC,MAChBF,EAAOE,CAAQ,KAAK,MAOhBC,IAAc,CACzBC,GACAC,MACmB;AACnB,QAAM,EAAE,cAAAC,MAAiBD,KAAW,CAAA;AAEpC,SAAO,CAAC,GAAGD,CAAO,EAAE,KAAK,CAACG,GAAGC,MAAM;AACjC,UAAMC,IAAYD,EAAE,QAAQD,EAAE;AAE9B,QAAIE,MAAc,EAAG,QAAOA;AAE5B,QAAIH,GAAc;AAChB,YAAMI,IAASH,EAAED,CAAY,GACvBK,IAASH,EAAEF,CAAY;AAE7B,UAAII,KAAU,QAAQC,KAAU;AAC9B,eAAO,OAAOD,CAAM,EAAE,cAAc,OAAOC,CAAM,CAAC;AAAA,IAEtD;AAEA,WAAO;AAAA,EACT,CAAC;AACH,GAEaC,IACX,CAAIC,MACJ,CAACC,MACC,CAAC,GAAGA,CAAI,EAAE,KAAKD,CAAS;;;;;;;;;;;;;ACvCrB,SAASE,EAAY;AAAA,EAC1B,OAAAC;AAAA,EACA,UAAAd;AAAA,EACA,YAAAe;AAAA,EACA,eAAAC;AACF,GAAqB;AACnB,QAAMC,IAAQlB,EAASC,CAAQ,GACzBkB,IAAWJ,EAAM,MAAM,MAAM,GAAG,CAAC,EAAE,YAAA;AAEzC,SACE,gBAAAK,EAAC,MAAA,EAAG,WAAW,GAAGC,EAAO,IAAI,IAAIJ,IAAgBI,EAAO,YAAY,EAAE,IACpE,UAAA;AAAA,IAAA,gBAAAC,EAAC,UAAK,WAAWD,EAAO,UACrB,UAAAH,KAAS,IAAIjB,CAAQ,GAAA,CACxB;AAAA,sBAEC,QAAA,EAAK,WAAWoB,EAAO,QACrB,YAAM,SACL,gBAAAC,EAAC,OAAA,EAAI,KAAKP,EAAM,QAAQ,KAAKA,EAAM,MAAA,CAAO,IAE1CI,GAEJ;AAAA,sBAEC,QAAA,EAAK,WAAWE,EAAO,OAAQ,YAAM,OAAM;AAAA,IAE5C,gBAAAD,EAAC,QAAA,EAAK,WAAWC,EAAO,OACrB,UAAA;AAAA,MAAAN,EAAM,MAAM,eAAe,OAAO;AAAA,MACnC,gBAAAO,EAAC,QAAA,EAAK,WAAWD,EAAO,WAAY,UAAAL,EAAA,CAAW;AAAA,IAAA,EAAA,CACjD;AAAA,EAAA,GACF;AAEJ;AC7BA,MAAMO,IAA0C;AAAA,EAC9C,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAEO,SAASC,EAAQ;AAAA,EACtB,SAAArB;AAAA,EACA,OAAAsB;AAAA,EACA,WAAAC,IAAY;AAAA,EACZ,aAAAC;AAAA,EACA,OAAAC;AACF,GAAiB;AACf,QAAMC,IAAS3B,EAAYC,CAAO,GAE5B2B,IAAiBF,IACnBC,EAAO,MAAM,GAAGD,CAAK,IACrBC,GAEEb,IAAaO,EAAaG,CAAS;AAEzC,SACE,gBAAAJ,EAAC,SAAI,WAAWD,EAAO,SACrB,UAAA,gBAAAD,EAAC,OAAA,EAAI,WAAWC,EAAO,WACpB,UAAA;AAAA,IAAAI,KAAS,gBAAAH,EAAC,MAAA,EAAG,WAAWD,EAAO,OAAQ,UAAAI,GAAM;AAAA,IAE9C,gBAAAH,EAAC,QAAG,WAAWD,EAAO,MACnB,UAAAS,EAAe,IAAI,CAACf,GAAOgB,MAAU;AACpC,YAAMd,IAAgBU,IAClBA,EAAYZ,GAAOgB,CAAK,IACxB;AAEJ,aACE,gBAAAT;AAAA,QAACR;AAAA,QAAA;AAAA,UAEC,OAAAC;AAAA,UACA,UAAUgB,IAAQ;AAAA,UAClB,YAAAf;AAAA,UACA,eAAAC;AAAA,QAAA;AAAA,QAJKF,EAAM;AAAA,MAAA;AAAA,IAOjB,CAAC,EAAA,CACH;AAAA,EAAA,EAAA,CACF,EAAA,CACF;AAEJ;"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(n,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("react/jsx-runtime")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime"],e):(n=typeof globalThis<"u"?globalThis:n||self,e(n.Tsukikage={},n.jsxRuntime))})(this,(function(n,e){"use strict";const u={1:"🥇",2:"🥈",3:"🥉"},p=s=>u[s]??null,h=(s,o)=>{const{secondaryKey:r}=o||{};return[...s].sort((a,i)=>{const l=i.score-a.score;if(l!==0)return l;if(r){const c=a[r],_=i[r];if(c!=null&&_!=null)return String(c).localeCompare(String(_))}return 0})},m=s=>o=>[...o].sort(s),t={wrapper:"_wrapper_1nrlo_1",container:"_container_1nrlo_5",title:"_title_1nrlo_16",list:"_list_1nrlo_35",item:"_item_1nrlo_44",position:"_position_1nrlo_75",avatar:"_avatar_1nrlo_83",label:"_label_1nrlo_105",score:"_score_1nrlo_112",scoreUnit:"_scoreUnit_1nrlo_118",highlight:"_highlight_1nrlo_125"};function g({entry:s,position:o,scoreLabel:r,isHighlighted:a}){const i=p(o),l=s.label.slice(0,2).toUpperCase();return e.jsxs("li",{className:`${t.item} ${a?t.highlight:""}`,children:[e.jsx("span",{className:t.position,children:i??`#${o}`}),e.jsx("span",{className:t.avatar,children:s.avatar?e.jsx("img",{src:s.avatar,alt:s.label}):l}),e.jsx("span",{className:t.label,children:s.label}),e.jsxs("span",{className:t.score,children:[s.score.toLocaleString("en-US"),e.jsx("span",{className:t.scoreUnit,children:r})]})]})}const S={1:"pts",2:"xp",3:"coins"};function b({entries:s,title:o,scoreType:r=1,highlightFn:a,limit:i}){const l=h(s),c=i?l.slice(0,i):l,_=S[r];return e.jsx("div",{className:t.wrapper,children:e.jsxs("div",{className:t.container,children:[o&&e.jsx("h2",{className:t.title,children:o}),e.jsx("ol",{className:t.list,children:c.map((d,f)=>{const v=a?a(d,f):!1;return e.jsx(g,{entry:d,position:f+1,scoreLabel:_,isHighlighted:v},d.id)})})]})})}n.Ranking=b,n.RankingItem=g,n.createSorter=m,n.getMedal=p,n.sortByScore=h,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})}));
|
|
2
|
+
//# sourceMappingURL=tsukikage.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsukikage.umd.cjs","sources":["../src/utils/ranking.utils.ts","../src/components/RankingItem.tsx","../src/components/Ranking.tsx"],"sourcesContent":["import type { RankingEntry } from \"../types/ranking.types\";\n\nconst MEDALS: Record<number, string> = {\n 1: \"🥇\",\n 2: \"🥈\",\n 3: \"🥉\",\n};\n\nexport const getMedal = (position: number): string | null => {\n return MEDALS[position] ?? null;\n};\n\ntype SortOptions = {\n secondaryKey?: keyof RankingEntry;\n};\n\nexport const sortByScore = (\n entries: RankingEntry[],\n options?: SortOptions\n): RankingEntry[] => {\n const { secondaryKey } = options || {};\n\n return [...entries].sort((a, b) => {\n const scoreDiff = b.score - a.score;\n\n if (scoreDiff !== 0) return scoreDiff;\n\n if (secondaryKey) {\n const aValue = a[secondaryKey];\n const bValue = b[secondaryKey];\n\n if (aValue != null && bValue != null) {\n return String(aValue).localeCompare(String(bValue));\n }\n }\n\n return 0;\n });\n};\n\nexport const createSorter =\n <T>(compareFn: (a: T, b: T) => number) =>\n (list: T[]): T[] =>\n [...list].sort(compareFn);","import type { RankingItemProps } from \"../types/ranking.types\";\nimport { getMedal } from \"../utils/ranking.utils\";\nimport styles from \"../css/Ranking.module.css\";\n\nexport function RankingItem({\n entry,\n position,\n scoreLabel,\n isHighlighted,\n}: RankingItemProps) {\n const medal = getMedal(position);\n const initials = entry.label.slice(0, 2).toUpperCase();\n\n return (\n <li className={`${styles.item} ${isHighlighted ? styles.highlight : \"\"}`}>\n <span className={styles.position}>\n {medal ?? `#${position}`}\n </span>\n\n <span className={styles.avatar}>\n {entry.avatar ? (\n <img src={entry.avatar} alt={entry.label} />\n ) : (\n initials\n )}\n </span>\n\n <span className={styles.label}>{entry.label}</span>\n\n <span className={styles.score}>\n {entry.score.toLocaleString(\"en-US\")}\n <span className={styles.scoreUnit}>{scoreLabel}</span>\n </span>\n </li>\n );\n}","import type { RankingProps, ScoreType } from \"../types/ranking.types\";\nimport { sortByScore } from \"../utils/ranking.utils\";\n// @ts-ignore: CSS module without type declarations\nimport styles from \"../css/Ranking.module.css\";\nimport { RankingItem } from \"./RankingItem\";\n\nconst SCORE_LABELS: Record<ScoreType, string> = {\n 1: \"pts\",\n 2: \"xp\",\n 3: \"coins\",\n};\n\nexport function Ranking({\n entries,\n title,\n scoreType = 1,\n highlightFn,\n limit,\n}: RankingProps) {\n const sorted = sortByScore(entries);\n\n const visibleEntries = limit\n ? sorted.slice(0, limit)\n : sorted;\n\n const scoreLabel = SCORE_LABELS[scoreType];\n\n return (\n <div className={styles.wrapper}>\n <div className={styles.container}>\n {title && <h2 className={styles.title}>{title}</h2>}\n\n <ol className={styles.list}>\n {visibleEntries.map((entry, index) => {\n const isHighlighted = highlightFn\n ? highlightFn(entry, index)\n : false;\n\n return (\n <RankingItem\n key={entry.id}\n entry={entry}\n position={index + 1}\n scoreLabel={scoreLabel}\n isHighlighted={isHighlighted}\n />\n );\n })}\n </ol>\n </div>\n </div>\n );\n}"],"names":["MEDALS","getMedal","position","sortByScore","entries","options","secondaryKey","b","scoreDiff","aValue","bValue","createSorter","compareFn","list","RankingItem","entry","scoreLabel","isHighlighted","medal","initials","jsxs","styles","jsx","SCORE_LABELS","Ranking","title","scoreType","highlightFn","limit","sorted","visibleEntries","index"],"mappings":"kSAEA,MAAMA,EAAiC,CACrC,EAAG,KACH,EAAG,KACH,EAAG,IACL,EAEaC,EAAYC,GAChBF,EAAOE,CAAQ,GAAK,KAOhBC,EAAc,CACzBC,EACAC,IACmB,CACnB,KAAM,CAAE,aAAAC,GAAiBD,GAAW,CAAA,EAEpC,MAAO,CAAC,GAAGD,CAAO,EAAE,KAAK,CAAC,EAAGG,IAAM,CACjC,MAAMC,EAAYD,EAAE,MAAQ,EAAE,MAE9B,GAAIC,IAAc,EAAG,OAAOA,EAE5B,GAAIF,EAAc,CAChB,MAAMG,EAAS,EAAEH,CAAY,EACvBI,EAASH,EAAED,CAAY,EAE7B,GAAIG,GAAU,MAAQC,GAAU,KAC9B,OAAO,OAAOD,CAAM,EAAE,cAAc,OAAOC,CAAM,CAAC,CAEtD,CAEA,MAAO,EACT,CAAC,CACH,EAEaC,EACPC,GACHC,GACC,CAAC,GAAGA,CAAI,EAAE,KAAKD,CAAS,gTCvCrB,SAASE,EAAY,CAC1B,MAAAC,EACA,SAAAb,EACA,WAAAc,EACA,cAAAC,CACF,EAAqB,CACnB,MAAMC,EAAQjB,EAASC,CAAQ,EACzBiB,EAAWJ,EAAM,MAAM,MAAM,EAAG,CAAC,EAAE,YAAA,EAEzC,OACEK,EAAAA,KAAC,KAAA,CAAG,UAAW,GAAGC,EAAO,IAAI,IAAIJ,EAAgBI,EAAO,UAAY,EAAE,GACpE,SAAA,CAAAC,EAAAA,IAAC,QAAK,UAAWD,EAAO,SACrB,SAAAH,GAAS,IAAIhB,CAAQ,EAAA,CACxB,QAEC,OAAA,CAAK,UAAWmB,EAAO,OACrB,WAAM,OACLC,EAAAA,IAAC,MAAA,CAAI,IAAKP,EAAM,OAAQ,IAAKA,EAAM,KAAA,CAAO,EAE1CI,EAEJ,QAEC,OAAA,CAAK,UAAWE,EAAO,MAAQ,WAAM,MAAM,EAE5CD,EAAAA,KAAC,OAAA,CAAK,UAAWC,EAAO,MACrB,SAAA,CAAAN,EAAM,MAAM,eAAe,OAAO,EACnCO,EAAAA,IAAC,OAAA,CAAK,UAAWD,EAAO,UAAY,SAAAL,CAAA,CAAW,CAAA,CAAA,CACjD,CAAA,EACF,CAEJ,CC7BA,MAAMO,EAA0C,CAC9C,EAAG,MACH,EAAG,KACH,EAAG,OACL,EAEO,SAASC,EAAQ,CACtB,QAAApB,EACA,MAAAqB,EACA,UAAAC,EAAY,EACZ,YAAAC,EACA,MAAAC,CACF,EAAiB,CACf,MAAMC,EAAS1B,EAAYC,CAAO,EAE5B0B,EAAiBF,EACnBC,EAAO,MAAM,EAAGD,CAAK,EACrBC,EAEEb,EAAaO,EAAaG,CAAS,EAEzC,OACEJ,EAAAA,IAAC,OAAI,UAAWD,EAAO,QACrB,SAAAD,EAAAA,KAAC,MAAA,CAAI,UAAWC,EAAO,UACpB,SAAA,CAAAI,GAASH,EAAAA,IAAC,KAAA,CAAG,UAAWD,EAAO,MAAQ,SAAAI,EAAM,EAE9CH,EAAAA,IAAC,MAAG,UAAWD,EAAO,KACnB,SAAAS,EAAe,IAAI,CAACf,EAAOgB,IAAU,CACpC,MAAMd,EAAgBU,EAClBA,EAAYZ,EAAOgB,CAAK,EACxB,GAEJ,OACET,EAAAA,IAACR,EAAA,CAEC,MAAAC,EACA,SAAUgB,EAAQ,EAClB,WAAAf,EACA,cAAAC,CAAA,EAJKF,EAAM,EAAA,CAOjB,CAAC,CAAA,CACH,CAAA,CAAA,CACF,CAAA,CACF,CAEJ"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface RankingEntry {
|
|
2
|
+
id: string | number;
|
|
3
|
+
label: string;
|
|
4
|
+
score: number;
|
|
5
|
+
avatar?: string;
|
|
6
|
+
}
|
|
7
|
+
export type ScoreType = 1 | 2 | 3;
|
|
8
|
+
export interface RankingProps {
|
|
9
|
+
entries: RankingEntry[];
|
|
10
|
+
title?: string;
|
|
11
|
+
scoreType?: ScoreType;
|
|
12
|
+
highlightFn?: (entry: RankingEntry, index: number) => boolean;
|
|
13
|
+
limit?: number;
|
|
14
|
+
}
|
|
15
|
+
export type RankingItemProps = {
|
|
16
|
+
entry: RankingEntry;
|
|
17
|
+
position: number;
|
|
18
|
+
scoreLabel: string;
|
|
19
|
+
isHighlighted?: boolean;
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RankingEntry } from '../types/ranking.types';
|
|
2
|
+
export declare const getMedal: (position: number) => string | null;
|
|
3
|
+
type SortOptions = {
|
|
4
|
+
secondaryKey?: keyof RankingEntry;
|
|
5
|
+
};
|
|
6
|
+
export declare const sortByScore: (entries: RankingEntry[], options?: SortOptions) => RankingEntry[];
|
|
7
|
+
export declare const createSorter: <T>(compareFn: (a: T, b: T) => number) => (list: T[]) => T[];
|
|
8
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tsukikage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "🏆 A sleek, customizable React ranking component library",
|
|
5
|
+
"author": "your-name",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"react",
|
|
9
|
+
"ranking",
|
|
10
|
+
"leaderboard",
|
|
11
|
+
"component",
|
|
12
|
+
"ui"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/your-username/tsukikage"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/your-username/tsukikage#readme",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"main": "./dist/tsukikage.umd.cjs",
|
|
24
|
+
"module": "./dist/tsukikage.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/tsukikage.js",
|
|
30
|
+
"require": "./dist/tsukikage.umd.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "vite",
|
|
35
|
+
"build": "tsc -b && vite build",
|
|
36
|
+
"lint": "eslint .",
|
|
37
|
+
"preview": "vite preview",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18.0.0",
|
|
42
|
+
"react-dom": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@eslint/js": "^9.39.1",
|
|
46
|
+
"@types/node": "^24.10.1",
|
|
47
|
+
"@types/react": "^19.2.7",
|
|
48
|
+
"@types/react-dom": "^19.2.3",
|
|
49
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
50
|
+
"eslint": "^9.39.1",
|
|
51
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
52
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
53
|
+
"globals": "^16.5.0",
|
|
54
|
+
"react": "^19.2.0",
|
|
55
|
+
"react-dom": "^19.2.0",
|
|
56
|
+
"typescript": "~5.9.3",
|
|
57
|
+
"typescript-eslint": "^8.48.0",
|
|
58
|
+
"vite": "^7.3.1",
|
|
59
|
+
"vite-plugin-dts": "^4.5.4"
|
|
60
|
+
}
|
|
61
|
+
}
|