@geenius/feedback 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.
Files changed (85) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  6. package/.github/dependabot.yml +11 -0
  7. package/.github/workflows/ci.yml +23 -0
  8. package/.github/workflows/release.yml +29 -0
  9. package/.nvmrc +1 -0
  10. package/.project/ACCOUNT.yaml +4 -0
  11. package/.project/IDEAS.yaml +7 -0
  12. package/.project/PROJECT.yaml +11 -0
  13. package/.project/ROADMAP.yaml +15 -0
  14. package/CHANGELOG.md +8 -0
  15. package/CODE_OF_CONDUCT.md +16 -0
  16. package/CONTRIBUTING.md +26 -0
  17. package/LICENSE +21 -0
  18. package/README.md +1 -0
  19. package/SECURITY.md +15 -0
  20. package/SUPPORT.md +8 -0
  21. package/package.json +75 -0
  22. package/packages/convex/package.json +42 -0
  23. package/packages/convex/src/index.ts +3 -0
  24. package/packages/convex/src/mutations.ts +88 -0
  25. package/packages/convex/src/queries.ts +78 -0
  26. package/packages/convex/src/schema.ts +47 -0
  27. package/packages/convex/tsconfig.json +18 -0
  28. package/packages/convex/tsup.config.ts +17 -0
  29. package/packages/react/README.md +1 -0
  30. package/packages/react/package.json +49 -0
  31. package/packages/react/src/components/FeedbackCard.tsx +51 -0
  32. package/packages/react/src/components/FeedbackForm.tsx +43 -0
  33. package/packages/react/src/components/FeedbackWidget.tsx +32 -0
  34. package/packages/react/src/components/NPSSurvey.tsx +62 -0
  35. package/packages/react/src/components/index.ts +4 -0
  36. package/packages/react/src/hooks/index.ts +5 -0
  37. package/packages/react/src/hooks/useFeedback.ts +23 -0
  38. package/packages/react/src/hooks/useFeedbackAdmin.ts +24 -0
  39. package/packages/react/src/hooks/useFeedbackForm.ts +35 -0
  40. package/packages/react/src/hooks/useNPS.ts +26 -0
  41. package/packages/react/src/index.tsx +13 -0
  42. package/packages/react/src/pages/FeedbackAdminPage.tsx +71 -0
  43. package/packages/react/src/pages/FeedbackPublicPage.tsx +42 -0
  44. package/packages/react/src/pages/FeedbackWidgetPage.tsx +25 -0
  45. package/packages/react/src/pages/index.ts +3 -0
  46. package/packages/react/tsconfig.json +19 -0
  47. package/packages/react/tsup.config.ts +12 -0
  48. package/packages/react-css/README.md +1 -0
  49. package/packages/react-css/package.json +36 -0
  50. package/packages/react-css/src/components/index.ts +5 -0
  51. package/packages/react-css/src/components/index.tsx +107 -0
  52. package/packages/react-css/src/hooks/index.ts +2 -0
  53. package/packages/react-css/src/index.tsx +5 -0
  54. package/packages/react-css/src/pages/FeedbackAdminPage.tsx +112 -0
  55. package/packages/react-css/src/pages/FeedbackPage.tsx +76 -0
  56. package/packages/react-css/src/styles.css +281 -0
  57. package/packages/react-css/tsconfig.json +19 -0
  58. package/packages/react-css/tsup.config.ts +10 -0
  59. package/packages/shared/README.md +1 -0
  60. package/packages/shared/package.json +44 -0
  61. package/packages/shared/src/__tests__/feedback.test.ts +72 -0
  62. package/packages/shared/src/config.ts +49 -0
  63. package/packages/shared/src/index.ts +111 -0
  64. package/packages/shared/src/types.ts +59 -0
  65. package/packages/shared/tsconfig.json +18 -0
  66. package/packages/shared/tsup.config.ts +11 -0
  67. package/packages/shared/vitest.config.ts +4 -0
  68. package/packages/solidjs/README.md +1 -0
  69. package/packages/solidjs/package.json +45 -0
  70. package/packages/solidjs/src/components.tsx +72 -0
  71. package/packages/solidjs/src/index.tsx +3 -0
  72. package/packages/solidjs/src/primitives.ts +49 -0
  73. package/packages/solidjs/tsconfig.json +20 -0
  74. package/packages/solidjs/tsup.config.ts +12 -0
  75. package/packages/solidjs-css/README.md +1 -0
  76. package/packages/solidjs-css/package.json +32 -0
  77. package/packages/solidjs-css/src/index.tsx +4 -0
  78. package/packages/solidjs-css/src/pages/FeedbackAdminPage.tsx +78 -0
  79. package/packages/solidjs-css/src/pages/FeedbackPage.tsx +65 -0
  80. package/packages/solidjs-css/src/primitives/index.ts +1 -0
  81. package/packages/solidjs-css/src/styles.css +281 -0
  82. package/packages/solidjs-css/tsconfig.json +20 -0
  83. package/packages/solidjs-css/tsup.config.ts +10 -0
  84. package/pnpm-workspace.yaml +2 -0
  85. package/tsconfig.json +23 -0
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@geenius-feedback/shared",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Geenius Feedback \u2014 Shared types & Convex schema",
7
+ "author": "Antigravity HQ",
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "./convex": "./src/convex.ts"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "clean": "rm -rf dist",
29
+ "type-check": "tsc --noEmit",
30
+ "prepublishOnly": "pnpm clean && pnpm build",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "test:coverage": "vitest run --coverage"
34
+ },
35
+ "devDependencies": {
36
+ "convex": "^1.34.0",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "~6.0.2",
39
+ "vitest": "^4.0.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=20.0.0"
43
+ }
44
+ }
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ STATUS_CONFIG, PRIORITY_CONFIG, TYPE_CONFIG,
4
+ FEEDBACK_TYPES, FEEDBACK_STATUSES, FEEDBACK_PRIORITIES,
5
+ getStatusTransitions, getNPSCategory, calcNPSScore,
6
+ } from '../index'
7
+
8
+ describe('Feedback Constants', () => {
9
+ it('FEEDBACK_TYPES has 4 types', () => {
10
+ expect(FEEDBACK_TYPES).toHaveLength(4)
11
+ expect(FEEDBACK_TYPES).toContain('bug')
12
+ expect(FEEDBACK_TYPES).toContain('feature')
13
+ })
14
+
15
+ it('FEEDBACK_STATUSES has 6 statuses', () => {
16
+ expect(FEEDBACK_STATUSES).toHaveLength(6)
17
+ expect(FEEDBACK_STATUSES).toContain('open')
18
+ expect(FEEDBACK_STATUSES).toContain('done')
19
+ })
20
+
21
+ it('STATUS_CONFIG maps every status to label+color', () => {
22
+ FEEDBACK_STATUSES.forEach(s => {
23
+ expect(STATUS_CONFIG[s]).toBeDefined()
24
+ expect(STATUS_CONFIG[s].label).toBeDefined()
25
+ })
26
+ })
27
+
28
+ it('PRIORITY_CONFIG has low through critical', () => {
29
+ FEEDBACK_PRIORITIES.forEach(p => {
30
+ expect(PRIORITY_CONFIG[p]).toBeDefined()
31
+ })
32
+ })
33
+ })
34
+
35
+ describe('Status Transitions', () => {
36
+ it('open can transition to under-review', () => {
37
+ const transitions = getStatusTransitions('open')
38
+ expect(transitions).toContain('under-review')
39
+ })
40
+
41
+ it('done has limited transitions', () => {
42
+ const transitions = getStatusTransitions('done')
43
+ expect(Array.isArray(transitions)).toBe(true)
44
+ })
45
+ })
46
+
47
+ describe('NPS Scoring', () => {
48
+ it('getNPSCategory detractor for 0-6', () => {
49
+ expect(getNPSCategory(0)).toBe('detractor')
50
+ expect(getNPSCategory(6)).toBe('detractor')
51
+ })
52
+
53
+ it('getNPSCategory passive for 7-8', () => {
54
+ expect(getNPSCategory(7)).toBe('passive')
55
+ expect(getNPSCategory(8)).toBe('passive')
56
+ })
57
+
58
+ it('getNPSCategory promoter for 9-10', () => {
59
+ expect(getNPSCategory(9)).toBe('promoter')
60
+ expect(getNPSCategory(10)).toBe('promoter')
61
+ })
62
+
63
+ it('calcNPSScore computes score from responses', () => {
64
+ const responses = [
65
+ { score: 10 }, { score: 9 }, { score: 5 }, { score: 3 },
66
+ ] as any[]
67
+ const nps = calcNPSScore(responses)
68
+ expect(typeof nps).toBe('number')
69
+ expect(nps).toBeGreaterThanOrEqual(-100)
70
+ expect(nps).toBeLessThanOrEqual(100)
71
+ })
72
+ })
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @fileoverview Configuration factory for Geenius Feedback
3
+ */
4
+
5
+ import type { FeedbackConfig } from './types'
6
+
7
+ export interface ConfigureFeedbackOptions {
8
+ floatingWidget?: boolean
9
+ npsEnabled?: boolean
10
+ npsInterval?: number
11
+ categories?: Array<'bug' | 'feature' | 'general' | 'suggestion'>
12
+ allowAnonymous?: boolean
13
+ }
14
+
15
+ /**
16
+ * Configure the feedback system with custom options
17
+ * @param options Configuration options for the feedback system
18
+ * @returns FeedbackConfig object ready for use
19
+ * @example
20
+ * ```ts
21
+ * const config = configureFeedback({
22
+ * floatingWidget: true,
23
+ * npsEnabled: true,
24
+ * npsInterval: 7,
25
+ * categories: ['bug', 'feature', 'general'],
26
+ * allowAnonymous: true,
27
+ * })
28
+ * ```
29
+ */
30
+ export function configureFeedback(options: ConfigureFeedbackOptions = {}): FeedbackConfig {
31
+ return {
32
+ floatingWidget: options.floatingWidget ?? true,
33
+ npsEnabled: options.npsEnabled ?? true,
34
+ npsInterval: options.npsInterval ?? 7,
35
+ categories: options.categories ?? ['bug', 'feature', 'general', 'suggestion'],
36
+ allowAnonymous: options.allowAnonymous ?? true,
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Default feedback configuration
42
+ */
43
+ export const defaultFeedbackConfig: FeedbackConfig = {
44
+ floatingWidget: true,
45
+ npsEnabled: true,
46
+ npsInterval: 7,
47
+ categories: ['bug', 'feature', 'general', 'suggestion'],
48
+ allowAnonymous: true,
49
+ }
@@ -0,0 +1,111 @@
1
+ export type {
2
+ FeedbackType, FeedbackStatus, FeedbackPriority, NPSCategory,
3
+ Attachment, FeedbackItem, NPSResponse, FeedbackConfig,
4
+ FeedbackStats, NPSStats,
5
+ } from './types'
6
+
7
+ export { configureFeedback, defaultFeedbackConfig } from './config'
8
+ export type { ConfigureFeedbackOptions } from './config'
9
+
10
+ import type { FeedbackStatus, FeedbackPriority, FeedbackType, NPSResponse, FeedbackStats, NPSStats, FeedbackItem } from './types'
11
+
12
+ // ─── Status Config ───────────────────────────────────
13
+ export const STATUS_CONFIG: Record<FeedbackStatus, { label: string; color: string; emoji: string }> = {
14
+ open: { label: 'Open', color: 'oklch(0.65 0.20 245)', emoji: '📬' },
15
+ 'under-review': { label: 'Under Review', color: 'oklch(0.72 0.18 60)', emoji: '🔍' },
16
+ planned: { label: 'Planned', color: 'oklch(0.70 0.22 280)', emoji: '📋' },
17
+ 'in-progress': { label: 'In Progress', color: 'oklch(0.65 0.20 265)', emoji: '🚧' },
18
+ done: { label: 'Done', color: 'oklch(0.72 0.18 155)', emoji: '✅' },
19
+ declined: { label: 'Declined', color: 'oklch(0.50 0.10 250)', emoji: '🚫' },
20
+ }
21
+
22
+ export const PRIORITY_CONFIG: Record<FeedbackPriority, { label: string; color: string }> = {
23
+ low: { label: 'Low', color: 'oklch(0.58 0.10 250)' },
24
+ medium: { label: 'Medium', color: 'oklch(0.72 0.18 60)' },
25
+ high: { label: 'High', color: 'oklch(0.68 0.22 35)' },
26
+ critical: { label: 'Critical', color: 'oklch(0.60 0.25 25)' },
27
+ }
28
+
29
+ export const TYPE_CONFIG: Record<FeedbackType, { label: string; icon: string; color: string }> = {
30
+ bug: { label: 'Bug', icon: '🐛', color: 'oklch(0.60 0.25 25)' },
31
+ feature: { label: 'Feature', icon: '✨', color: 'oklch(0.70 0.22 280)' },
32
+ suggestion: { label: 'Suggestion', icon: '💡', color: 'oklch(0.72 0.18 60)' },
33
+ general: { label: 'General', icon: '💬', color: 'oklch(0.65 0.20 245)' },
34
+ }
35
+
36
+ export const FEEDBACK_TYPES: FeedbackType[] = ['bug', 'feature', 'suggestion', 'general']
37
+ export const FEEDBACK_STATUSES: FeedbackStatus[] = ['open', 'under-review', 'planned', 'in-progress', 'done', 'declined']
38
+ export const FEEDBACK_PRIORITIES: FeedbackPriority[] = ['low', 'medium', 'high', 'critical']
39
+
40
+ // ─── Status Transitions ─────────────────────────────
41
+ const STATUS_TRANSITIONS: Record<FeedbackStatus, FeedbackStatus[]> = {
42
+ open: ['under-review', 'planned', 'declined'],
43
+ 'under-review': ['planned', 'in-progress', 'declined'],
44
+ planned: ['in-progress', 'declined'],
45
+ 'in-progress': ['done', 'planned'],
46
+ done: ['open'],
47
+ declined: ['open'],
48
+ }
49
+
50
+ export function getStatusTransitions(status: FeedbackStatus): FeedbackStatus[] {
51
+ return STATUS_TRANSITIONS[status] ?? []
52
+ }
53
+
54
+ // ─── NPS Utilities ───────────────────────────────────
55
+ export function getNPSCategory(score: number): 'detractor' | 'passive' | 'promoter' {
56
+ if (score <= 6) return 'detractor'
57
+ if (score <= 8) return 'passive'
58
+ return 'promoter'
59
+ }
60
+
61
+ export function calcNPSScore(responses: NPSResponse[]): number {
62
+ if (responses.length === 0) return 0
63
+ let promoters = 0, detractors = 0
64
+ for (const r of responses) {
65
+ const cat = getNPSCategory(r.score)
66
+ if (cat === 'promoter') promoters++
67
+ else if (cat === 'detractor') detractors++
68
+ }
69
+ return Math.round(((promoters - detractors) / responses.length) * 100)
70
+ }
71
+
72
+ export function calcNPSStats(responses: NPSResponse[]): NPSStats {
73
+ let promoters = 0, passives = 0, detractors = 0, totalScore = 0
74
+ for (const r of responses) {
75
+ totalScore += r.score
76
+ const cat = getNPSCategory(r.score)
77
+ if (cat === 'promoter') promoters++
78
+ else if (cat === 'passive') passives++
79
+ else detractors++
80
+ }
81
+ return {
82
+ averageScore: responses.length > 0 ? Math.round((totalScore / responses.length) * 10) / 10 : 0,
83
+ totalResponses: responses.length,
84
+ promoters, passives, detractors,
85
+ npsScore: calcNPSScore(responses),
86
+ }
87
+ }
88
+
89
+ export function calcFeedbackStats(items: FeedbackItem[]): FeedbackStats {
90
+ const byType = { bug: 0, feature: 0, suggestion: 0, general: 0 }
91
+ const byStatus = { open: 0, 'under-review': 0, planned: 0, 'in-progress': 0, done: 0, declined: 0 }
92
+ const byPriority = { low: 0, medium: 0, high: 0, critical: 0 }
93
+ for (const item of items) {
94
+ byType[item.type]++
95
+ byStatus[item.status]++
96
+ byPriority[item.priority]++
97
+ }
98
+ return { total: items.length, byType, byStatus, byPriority }
99
+ }
100
+
101
+ export function formatRelativeTime(dateStr: string): string {
102
+ const diff = Date.now() - new Date(dateStr).getTime()
103
+ const mins = Math.floor(diff / 60000)
104
+ if (mins < 1) return 'just now'
105
+ if (mins < 60) return `${mins}m ago`
106
+ const hrs = Math.floor(mins / 60)
107
+ if (hrs < 24) return `${hrs}h ago`
108
+ const days = Math.floor(hrs / 24)
109
+ if (days < 30) return `${days}d ago`
110
+ return new Date(dateStr).toLocaleDateString()
111
+ }
@@ -0,0 +1,59 @@
1
+ export type FeedbackType = 'bug' | 'feature' | 'general' | 'suggestion'
2
+ export type FeedbackStatus = 'open' | 'under-review' | 'planned' | 'in-progress' | 'done' | 'declined'
3
+ export type FeedbackPriority = 'low' | 'medium' | 'high' | 'critical'
4
+ export type NPSCategory = 'detractor' | 'passive' | 'promoter'
5
+
6
+ export interface Attachment { url: string; name: string; size: number }
7
+
8
+ export interface FeedbackItem {
9
+ id: string
10
+ type: FeedbackType
11
+ title: string
12
+ description: string
13
+ status: FeedbackStatus
14
+ priority: FeedbackPriority
15
+ userId?: string
16
+ userEmail?: string
17
+ userName?: string
18
+ url?: string
19
+ browser?: string
20
+ os?: string
21
+ tags: string[]
22
+ votes: number
23
+ createdAt: string
24
+ updatedAt: string
25
+ attachments?: Attachment[]
26
+ adminNote?: string
27
+ }
28
+
29
+ export interface NPSResponse {
30
+ id: string
31
+ score: number
32
+ comment?: string
33
+ userId?: string
34
+ submittedAt: string
35
+ }
36
+
37
+ export interface FeedbackConfig {
38
+ floatingWidget: boolean
39
+ npsEnabled: boolean
40
+ npsInterval: number
41
+ categories: FeedbackType[]
42
+ allowAnonymous: boolean
43
+ }
44
+
45
+ export interface FeedbackStats {
46
+ total: number
47
+ byType: Record<FeedbackType, number>
48
+ byStatus: Record<FeedbackStatus, number>
49
+ byPriority: Record<FeedbackPriority, number>
50
+ }
51
+
52
+ export interface NPSStats {
53
+ averageScore: number
54
+ totalResponses: number
55
+ promoters: number
56
+ passives: number
57
+ detractors: number
58
+ npsScore: number
59
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "target": "ES2022",
12
+ "module": "ESNext",
13
+ "moduleResolution": "bundler"
14
+ },
15
+ "include": [
16
+ "src"
17
+ ]
18
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: { index: 'src/index.ts' },
5
+ outDir: 'dist',
6
+ format: ['esm'],
7
+ dts: true,
8
+ sourcemap: true,
9
+ clean: true,
10
+ treeshake: true,
11
+ })
@@ -0,0 +1,4 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ export default defineConfig({
3
+ test: { globals: true, environment: 'node' },
4
+ })
@@ -0,0 +1 @@
1
+ # ✦ @geenius-feedback/solidjs\n\n> Geenius Feedback — SolidJS components & primitives\n\n---\n\n## Overview\nBuilt with Steve Jobs-level minimalism and Jony Ive-level craftsmanship, this package is designed to deliver unparalleled developer experience (DX) and rock-solid performance.\n\n## Installation\n\n```bash\npnpm add @geenius-feedback/solidjs\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-feedback/solidjs';\n\n// Initialize the module with absolute precision\ninit({\n mode: 'premium',\n});\n```\n\n## Architecture\n- **Zero-config**: It just works.\n- **Strictly Typed**: Fully written in TypeScript for flawless IntelliSense.\n- **Framework Agnostic**: seamlessly integrates into the Geenius ecosystem.\n\n---\n\n*Designed by Antigravity HQ*\n
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@geenius-feedback/solidjs",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Geenius Feedback — SolidJS components & primitives",
7
+ "author": "Antigravity HQ",
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "clean": "rm -rf dist",
28
+ "type-check": "tsc --noEmit",
29
+ "prepublishOnly": "pnpm clean && pnpm build"
30
+ },
31
+ "dependencies": {
32
+ "@geenius-feedback/shared": "workspace:*"
33
+ },
34
+ "devDependencies": {
35
+ "solid-js": "^1.9.0",
36
+ "tsup": "^8.5.1",
37
+ "typescript": "~6.0.2"
38
+ },
39
+ "peerDependencies": {
40
+ "solid-js": "^1.8.0 || ^1.9.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=20.0.0"
44
+ }
45
+ }
@@ -0,0 +1,72 @@
1
+ import { Show, For, createSignal } from 'solid-js'
2
+ import type { FeedbackItem, FeedbackType, FeedbackStatus, FeedbackPriority } from '@geenius-feedback/shared'
3
+ import { STATUS_CONFIG, PRIORITY_CONFIG, TYPE_CONFIG, FEEDBACK_TYPES, formatRelativeTime } from '@geenius-feedback/shared'
4
+
5
+ export function StatusBadge(props: { status: FeedbackStatus }) {
6
+ const cfg = () => STATUS_CONFIG[props.status]
7
+ return <span class="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[10px] font-medium" style={{ background: `${cfg().color}22`, color: cfg().color }}><span>{cfg().emoji}</span>{cfg().label}</span>
8
+ }
9
+
10
+ export function PriorityBadge(props: { priority: FeedbackPriority }) {
11
+ const cfg = () => PRIORITY_CONFIG[props.priority]
12
+ return <span class="rounded-full px-2 py-0.5 text-[10px] font-medium" style={{ background: `${cfg().color}22`, color: cfg().color }}>{cfg().label}</span>
13
+ }
14
+
15
+ export function TypeBadge(props: { type: FeedbackType }) {
16
+ const cfg = () => TYPE_CONFIG[props.type]
17
+ return <span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium" style={{ background: `${cfg().color}22`, color: cfg().color }}><span>{cfg().icon}</span>{cfg().label}</span>
18
+ }
19
+
20
+ export function FeedbackCard(props: { item: FeedbackItem; onVote?: (id: string) => void; hasVoted?: boolean }) {
21
+ return (
22
+ <div class="group flex gap-3 rounded-xl border border-white/8 bg-white/[0.02] p-4 hover:border-indigo-500/20">
23
+ <Show when={props.onVote}><button type="button" onClick={() => props.onVote?.(props.item.id)}
24
+ class={`flex flex-col items-center gap-0.5 rounded-lg px-2 py-1.5 text-xs font-bold ${props.hasVoted ? 'bg-indigo-500/15 text-indigo-400' : 'bg-white/5 text-white/30 hover:bg-white/10'}`}>
25
+ <span class="text-sm">{props.hasVoted ? '▲' : '△'}</span><span>{props.item.votes}</span>
26
+ </button></Show>
27
+ <div class="flex-1 min-w-0">
28
+ <div class="flex items-center gap-2 mb-1 flex-wrap"><TypeBadge type={props.item.type} /><StatusBadge status={props.item.status} /></div>
29
+ <h3 class="text-sm font-semibold text-white/90 mb-0.5 truncate">{props.item.title}</h3>
30
+ <p class="text-xs text-white/40 line-clamp-2">{props.item.description}</p>
31
+ <div class="mt-2 text-[10px] text-white/25">{formatRelativeTime(props.item.createdAt)}</div>
32
+ </div>
33
+ </div>
34
+ )
35
+ }
36
+
37
+ export function FeedbackWidget(props: { onSubmit: (data: { type: FeedbackType; title: string; description: string }) => Promise<string> }) {
38
+ const [isOpen, setIsOpen] = createSignal(false)
39
+ const [type, setType] = createSignal<FeedbackType>('general')
40
+ const [title, setTitle] = createSignal(''); const [desc, setDesc] = createSignal('')
41
+ const [submitting, setSubmitting] = createSignal(false); const [done, setDone] = createSignal(false)
42
+ const submit = async () => { setSubmitting(true); try { await props.onSubmit({ type: type(), title: title(), description: desc() }); setDone(true) } finally { setSubmitting(false) } }
43
+ return (<>
44
+ <button type="button" onClick={() => setIsOpen(true)} class="fixed top-1/2 right-0 -translate-y-1/2 z-40 rounded-l-xl bg-indigo-600 px-2 py-4 text-white shadow-lg hover:bg-indigo-500" style={{ "writing-mode": "vertical-rl" }}><span class="text-xs font-medium tracking-wider">Feedback</span></button>
45
+ <Show when={isOpen()}>
46
+ <div class="fixed inset-0 z-50 flex justify-end" onClick={() => setIsOpen(false)}>
47
+ <div class="absolute inset-0 bg-black/50 backdrop-blur-sm" />
48
+ <div class="relative w-full max-w-md bg-[#0f0f17] border-l border-white/10 p-6" onClick={e => e.stopPropagation()}>
49
+ <div class="mb-6 flex items-center justify-between"><h2 class="text-lg font-bold text-white">Send Feedback</h2><button type="button" onClick={() => setIsOpen(false)} class="text-white/30 hover:text-white/50">✕</button></div>
50
+ <Show when={!done()} fallback={<div class="text-center py-8"><div class="text-4xl mb-3">🎉</div><p class="text-sm text-white/80">Thank you!</p></div>}>
51
+ <div class="flex gap-1.5 mb-4"><For each={FEEDBACK_TYPES}>{t => <button type="button" onClick={() => setType(t)} class={`flex items-center gap-1 rounded-lg px-3 py-2 text-xs font-medium ${type() === t ? 'text-white' : 'bg-white/5 text-white/50'}`} style={type() === t ? { background: TYPE_CONFIG[t].color } : undefined}>{TYPE_CONFIG[t].icon} {TYPE_CONFIG[t].label}</button>}</For></div>
52
+ <input type="text" placeholder="Title…" value={title()} onInput={e => setTitle((e.target as HTMLInputElement).value)} class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 outline-none mb-3" />
53
+ <textarea placeholder="Description…" value={desc()} onInput={e => setDesc((e.target as HTMLTextAreaElement).value)} rows={4} class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 outline-none resize-none mb-4" />
54
+ <button type="button" onClick={submit} disabled={submitting()} class="w-full rounded-lg bg-indigo-600 py-2.5 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50">{submitting() ? 'Submitting…' : 'Submit'}</button>
55
+ </Show>
56
+ </div>
57
+ </div>
58
+ </Show>
59
+ </>)
60
+ }
61
+
62
+ export function NPSSurvey(props: { onSubmit: (score: number, comment?: string) => void; onDismiss: () => void }) {
63
+ const [score, setScore] = createSignal<number | null>(null)
64
+ const [comment, setComment] = createSignal(''); const [step, setStep] = createSignal<'score'|'comment'|'thanks'>('score')
65
+ return (
66
+ <div class="fixed bottom-6 right-6 z-50 w-96 rounded-2xl border border-white/10 bg-[#111118] p-6 shadow-2xl">
67
+ <Show when={step() === 'score'}><div><div class="mb-1 flex items-center justify-between"><h3 class="text-sm font-semibold text-white/90">How likely to recommend us?</h3><button type="button" onClick={props.onDismiss} class="text-white/20">✕</button></div><div class="flex gap-1 mt-3"><For each={Array.from({length:11},(_,i)=>i)}>{i => <button type="button" onClick={() => { setScore(i); setStep('comment') }} class="flex-1 rounded-lg py-2.5 text-xs font-bold bg-white/5 text-white/50 hover:bg-white/10">{i}</button>}</For></div></div></Show>
68
+ <Show when={step() === 'comment'}><div><p class="text-sm font-semibold text-white/90 mb-3">Score: {score()}</p><textarea value={comment()} onInput={e => setComment((e.target as HTMLTextAreaElement).value)} rows={3} class="w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none resize-none mb-3" /><button type="button" onClick={() => { props.onSubmit(score()!, comment() || undefined); setStep('thanks') }} class="w-full rounded-lg bg-indigo-600 py-2 text-xs text-white">Submit</button></div></Show>
69
+ <Show when={step() === 'thanks'}><div class="text-center py-4"><div class="text-3xl mb-2">🎉</div><p class="text-sm text-white/80">Thank you!</p></div></Show>
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,3 @@
1
+ export { createFeedback, createFeedbackForm, createNPS, createFeedbackAdmin } from './primitives'
2
+ export { StatusBadge, PriorityBadge, TypeBadge, FeedbackCard, FeedbackWidget, NPSSurvey } from './components'
3
+ export type { FeedbackItem, NPSResponse, FeedbackConfig, FeedbackStats, NPSStats, FeedbackType, FeedbackStatus, FeedbackPriority } from '@geenius-feedback/shared'
@@ -0,0 +1,49 @@
1
+ import { createSignal, createMemo } from 'solid-js'
2
+ import type { FeedbackItem, FeedbackType, FeedbackStatus, FeedbackPriority, FeedbackStats, NPSResponse } from '@geenius-feedback/shared'
3
+ import { calcFeedbackStats, calcNPSStats } from '@geenius-feedback/shared'
4
+
5
+ export function createFeedback(items: () => FeedbackItem[] | undefined) {
6
+ const [typeFilter, setTypeFilter] = createSignal<FeedbackType | undefined>()
7
+ const [search, setSearch] = createSignal('')
8
+ const [sort, setSort] = createSignal<'newest' | 'votes'>('newest')
9
+ const isLoading = createMemo(() => items() === undefined)
10
+ const stats = createMemo(() => calcFeedbackStats(items() ?? []))
11
+ const filtered = createMemo(() => {
12
+ let r = items() ?? []; const tf = typeFilter(); if (tf) r = r.filter(i => i.type === tf)
13
+ const q = search().toLowerCase(); if (q) r = r.filter(i => i.title.toLowerCase().includes(q) || i.description.toLowerCase().includes(q))
14
+ if (sort() === 'votes') r = [...r].sort((a, b) => b.votes - a.votes)
15
+ return r
16
+ })
17
+ return { items: filtered, stats, isLoading, typeFilter, setTypeFilter, search, setSearch, sort, setSort }
18
+ }
19
+
20
+ export function createFeedbackForm(submitFn: (data: { type: FeedbackType; title: string; description: string }) => Promise<string>) {
21
+ const [type, setType] = createSignal<FeedbackType>('general')
22
+ const [title, setTitle] = createSignal(''); const [desc, setDesc] = createSignal('')
23
+ const [isSubmitting, setIsSubmitting] = createSignal(false); const [error, setError] = createSignal<string | null>(null); const [success, setSuccess] = createSignal(false)
24
+ const submit = async () => {
25
+ if (!title().trim()) { setError('Title required'); return }; if (desc().length < 10) { setError('Description must be 10+ chars'); return }
26
+ setIsSubmitting(true); setError(null)
27
+ try { await submitFn({ type: type(), title: title(), description: desc() }); setSuccess(true); setTitle(''); setDesc('') }
28
+ catch (e) { setError(e instanceof Error ? e.message : 'Failed') } finally { setIsSubmitting(false) }
29
+ }
30
+ const reset = () => { setType('general'); setTitle(''); setDesc(''); setError(null); setSuccess(false) }
31
+ return { type, setType, title, setTitle, description: desc, setDescription: setDesc, submit, isSubmitting, error, success, reset }
32
+ }
33
+
34
+ export function createNPS(submitFn: (score: number, comment?: string) => Promise<void>) {
35
+ const [isSubmitting, setIsSubmitting] = createSignal(false)
36
+ const [showSurvey, setShowSurvey] = createSignal(typeof window !== 'undefined' && !localStorage.getItem('geenius-nps-last-shown'))
37
+ const submit = async (score: number, comment?: string) => {
38
+ setIsSubmitting(true); try { await submitFn(score, comment); localStorage.setItem('geenius-nps-last-shown', String(Date.now())); setShowSurvey(false) } finally { setIsSubmitting(false) }
39
+ }
40
+ const dismiss = () => { setShowSurvey(false); localStorage.setItem('geenius-nps-last-shown', String(Date.now())) }
41
+ return { submit, isSubmitting, showSurvey, dismiss }
42
+ }
43
+
44
+ export function createFeedbackAdmin(items: () => FeedbackItem[] | undefined, mutations: { updateStatus: (id: string, s: FeedbackStatus, note?: string) => Promise<void>; updatePriority: (id: string, p: FeedbackPriority) => Promise<void>; deleteFeedback: (id: string) => Promise<void> }) {
45
+ const [search, setSearch] = createSignal('')
46
+ const stats = createMemo(() => calcFeedbackStats(items() ?? []))
47
+ const filtered = createMemo(() => { const q = search().toLowerCase(); return q ? (items() ?? []).filter(i => i.title.toLowerCase().includes(q)) : (items() ?? []) })
48
+ return { items: filtered, stats, search, setSearch, isLoading: createMemo(() => items() === undefined), ...mutations }
49
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "jsx": "preserve",
7
+ "jsxImportSource": "solid-js",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "target": "ES2022",
14
+ "module": "ESNext",
15
+ "moduleResolution": "bundler"
16
+ },
17
+ "include": [
18
+ "src"
19
+ ]
20
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: { index: 'src/index.tsx' },
5
+ outDir: 'dist',
6
+ format: ['esm'],
7
+ dts: true,
8
+ sourcemap: true,
9
+ clean: true,
10
+ treeshake: true,
11
+ external: ['solid-js'],
12
+ })
@@ -0,0 +1 @@
1
+ # ✦ @geenius-feedback/solidjs-css\n\n> Geenius Feedback — SolidJS primitives (vanilla CSS variant)\n\n---\n\n## Overview\nBuilt with Steve Jobs-level minimalism and Jony Ive-level craftsmanship, this package is designed to deliver unparalleled developer experience (DX) and rock-solid performance.\n\n## Installation\n\n```bash\npnpm add @geenius-feedback/solidjs-css\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-feedback/solidjs-css';\n\n// Initialize the module with absolute precision\ninit({\n mode: 'premium',\n});\n```\n\n## Architecture\n- **Zero-config**: It just works.\n- **Strictly Typed**: Fully written in TypeScript for flawless IntelliSense.\n- **Framework Agnostic**: seamlessly integrates into the Geenius ecosystem.\n\n---\n\n*Designed by Antigravity HQ*\n
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@geenius-feedback/solidjs-css",
3
+ "version": "0.1.0",
4
+ "description": "Geenius Feedback — SolidJS primitives (vanilla CSS variant)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "restricted"
9
+ },
10
+ "main": "./src/index.tsx",
11
+ "types": "./src/index.tsx",
12
+ "exports": {
13
+ ".": "./src/index.tsx"
14
+ },
15
+ "scripts": {
16
+ "type-check": "tsc --noEmit"
17
+ },
18
+ "peerDependencies": {
19
+ "solid-js": ">=1.0.0"
20
+ },
21
+ "dependencies": {
22
+ "@geenius-feedback/shared": "workspace:*"
23
+ },
24
+ "devDependencies": {
25
+ "solid-js": "^1.9.4",
26
+ "typescript": "~6.0.2"
27
+ },
28
+ "author": "Antigravity HQ",
29
+ "engines": {
30
+ "node": ">=20.0.0"
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ export { createFeedback, createFeedbackForm, createNPS, createFeedbackAdmin } from '@geenius-feedback/solidjs'
2
+ export { StatusBadge, PriorityBadge, TypeBadge, FeedbackCard, FeedbackWidget, NPSSurvey } from '@geenius-feedback/solidjs'
3
+ import './styles.css'
4
+ export type { FeedbackItem, NPSResponse, FeedbackConfig, FeedbackStats, NPSStats, FeedbackType, FeedbackStatus, FeedbackPriority } from '@geenius-feedback/shared'