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 ADDED
@@ -0,0 +1,160 @@
1
+ # 🏆 tsukikage
2
+
3
+ > **月影** — *moonlight* in Japanese. A sleek, fully customizable React ranking component library.
4
+
5
+ [![npm version](https://img.shields.io/badge/version-0.0.0-violet?style=flat-square)](https://www.npmjs.com/)
6
+ [![license](https://img.shields.io/badge/license-MIT-blueviolet?style=flat-square)](./LICENSE)
7
+ [![react](https://img.shields.io/badge/react-19%2B-61DAFB?style=flat-square&logo=react)](https://react.dev/)
8
+ [![typescript](https://img.shields.io/badge/TypeScript-ready-3178C6?style=flat-square&logo=typescript)](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>
@@ -0,0 +1,2 @@
1
+ import { RankingProps } from '../types/ranking.types';
2
+ export declare function Ranking({ entries, title, scoreType, highlightFn, limit, }: RankingProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import { RankingItemProps } from '../types/ranking.types';
2
+ export declare function RankingItem({ entry, position, scoreLabel, isHighlighted, }: RankingItemProps): import("react/jsx-runtime").JSX.Element;
@@ -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":"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"}
@@ -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
+ }