@dilipod/ui 0.4.4 → 0.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dilipod/ui",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Dilipod Design System - Shared UI components and styles",
5
5
  "author": "Dilipod <hello@dilipod.com>",
6
6
  "license": "MIT",
@@ -1,9 +1,8 @@
1
1
  'use client'
2
2
 
3
3
  import { useState } from 'react'
4
- import { Card, CardContent, CardHeader, CardTitle } from './card'
4
+ import { Card, CardContent } from './card'
5
5
  import { Button } from './button'
6
- import { IconBox } from './icon-box'
7
6
  import { toast } from './use-toast'
8
7
  import { cn } from '../lib/utils'
9
8
 
@@ -138,6 +137,13 @@ export function ImpactMetricsForm({
138
137
  // Calculate hours saved per year based on FTE
139
138
  const hoursSavedPerYear = Math.round(metrics.fte_equivalent * HOURS_PER_FTE_YEAR)
140
139
 
140
+ // Calculate implied frequency based on time per task and FTE%
141
+ const timePerTaskHours = metrics.time_saved_minutes_per_run / 60
142
+ const impliedFrequencyPerYear = timePerTaskHours > 0
143
+ ? Math.round(hoursSavedPerYear / timePerTaskHours)
144
+ : 0
145
+ const impliedFrequencyPerMonth = Math.round(impliedFrequencyPerYear / 12)
146
+
141
147
  // Calculate labor savings (before worker cost)
142
148
  const laborSavingsPerYear = metrics.fte_equivalent * HOURS_PER_FTE_YEAR * metrics.hourly_rate_euros
143
149
 
@@ -145,17 +151,11 @@ export function ImpactMetricsForm({
145
151
  const netAnnualSavings = laborSavingsPerYear - workerCostPerYear
146
152
 
147
153
  return (
148
- <Card className={cn("border-[var(--cyan)]/20 bg-gradient-to-br from-white to-[var(--cyan)]/5", className)}>
149
- <CardHeader className="pb-3">
150
- <div className="flex items-center justify-between">
151
- <CardTitle className="flex items-center gap-2">
152
- <IconBox size="sm">
153
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256">
154
- <path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-88a16,16,0,1,1-16-16A16,16,0,0,1,144,128Zm-56,0a16,16,0,1,1-16-16A16,16,0,0,1,88,128Zm112,0a16,16,0,1,1-16-16A16,16,0,0,1,200,128Z"/>
155
- </svg>
156
- </IconBox>
157
- Impact Metrics (ROI)
158
- </CardTitle>
154
+ <Card className={cn("", className)}>
155
+ <CardContent className="p-5">
156
+ {/* Header */}
157
+ <div className="flex items-center justify-between mb-4">
158
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Impact Metrics (ROI)</p>
159
159
  <div className="flex items-center gap-2">
160
160
  {isEditing ? (
161
161
  <>
@@ -187,127 +187,104 @@ export function ImpactMetricsForm({
187
187
  )}
188
188
  </div>
189
189
  </div>
190
- </CardHeader>
191
- <CardContent>
192
- <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
190
+
191
+ {/* Metrics Grid */}
192
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
193
193
  {/* Time per task */}
194
- <div className="flex items-start gap-3">
195
- <div className="p-2 rounded-sm bg-[var(--cyan)]/10 shrink-0">
196
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256" className="text-[var(--cyan)]">
197
- <path d="M128,40a96,96,0,1,0,96,96A96.11,96.11,0,0,0,128,40Zm0,176a80,80,0,1,1,80-80A80.09,80.09,0,0,1,128,216ZM173.66,90.34a8,8,0,0,1,0,11.32l-40,40a8,8,0,0,1-11.32-11.32l40-40A8,8,0,0,1,173.66,90.34ZM96,16a8,8,0,0,1,8-8h48a8,8,0,0,1,0,16H104A8,8,0,0,1,96,16Z"/>
198
- </svg>
199
- </div>
200
- <div className="flex-1">
201
- <label className="text-sm text-muted-foreground block mb-1">Time per Task</label>
202
- {isEditing ? (
203
- <div className="flex items-center gap-2">
204
- <input
205
- type="number"
206
- value={metrics.time_saved_minutes_per_run}
207
- onChange={(e) => setMetrics(prev => ({
208
- ...prev,
209
- time_saved_minutes_per_run: parseInt(e.target.value) || 0
210
- }))}
211
- className="w-16 px-2 py-1 text-lg font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
212
- min="0"
213
- />
214
- <span className="text-muted-foreground">min</span>
215
- </div>
216
- ) : (
217
- <p className="text-2xl font-bold">
218
- {metrics.time_saved_minutes_per_run} <span className="text-base font-normal text-muted-foreground">min</span>
219
- </p>
220
- )}
221
- <p className="text-xs text-muted-foreground mt-1">How long manually</p>
222
- </div>
194
+ <div>
195
+ <p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Time per Task</p>
196
+ {isEditing ? (
197
+ <div className="flex items-baseline gap-1">
198
+ <input
199
+ type="number"
200
+ value={metrics.time_saved_minutes_per_run}
201
+ onChange={(e) => setMetrics(prev => ({
202
+ ...prev,
203
+ time_saved_minutes_per_run: parseInt(e.target.value) || 0
204
+ }))}
205
+ className="w-16 px-2 py-1 text-2xl font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
206
+ min="0"
207
+ />
208
+ <span className="text-sm text-muted-foreground">min</span>
209
+ </div>
210
+ ) : (
211
+ <p className="text-2xl font-bold">
212
+ {metrics.time_saved_minutes_per_run}<span className="text-sm font-normal text-muted-foreground ml-1">min</span>
213
+ </p>
214
+ )}
223
215
  </div>
224
216
 
225
217
  {/* Manual cost */}
226
- <div className="flex items-start gap-3">
227
- <div className="p-2 rounded-sm bg-[var(--cyan)]/10 shrink-0">
228
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256" className="text-[var(--cyan)]">
229
- <path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm0-144a8,8,0,0,1,8,8v4.4c14.25,3.14,24,14.43,24,30.6,0,4.42-3.58,8-8,8s-8-3.58-8-8c0-8.64-7.18-13-16-13s-16,4.36-16,13,7.18,13,16,13c17.64,0,32,11.35,32,29,0,16.17-9.75,27.46-24,30.6V192a8,8,0,0,1-16,0v-4.4c-14.25-3.14-24-14.43-24-30.6a8,8,0,0,1,16,0c0,8.64,7.18,13,16,13s16-4.36,16-13-7.18-13-16-13c-17.64,0-32-11.35-32-29,0-16.17,9.75-27.46,24-30.6V80A8,8,0,0,1,128,72Z"/>
230
- </svg>
231
- </div>
232
- <div className="flex-1">
233
- <label className="text-sm text-muted-foreground block mb-1">Manual Cost</label>
234
- {isEditing ? (
235
- <div className="flex items-center gap-2">
236
- <span className="text-muted-foreground">€</span>
237
- <input
238
- type="number"
239
- value={metrics.hourly_rate_euros}
240
- onChange={(e) => setMetrics(prev => ({
241
- ...prev,
242
- hourly_rate_euros: parseFloat(e.target.value) || 0
243
- }))}
244
- className="w-16 px-2 py-1 text-lg font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
245
- min="0"
246
- step="0.5"
247
- />
248
- <span className="text-muted-foreground">/hr</span>
249
- </div>
250
- ) : (
251
- <p className="text-2xl font-bold">
252
- €{metrics.hourly_rate_euros} <span className="text-base font-normal text-muted-foreground">/hr</span>
253
- </p>
254
- )}
255
- <p className="text-xs text-muted-foreground mt-1">Employee hourly cost</p>
256
- </div>
218
+ <div>
219
+ <p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Manual Cost</p>
220
+ {isEditing ? (
221
+ <div className="flex items-baseline gap-1">
222
+ <span className="text-sm text-muted-foreground">€</span>
223
+ <input
224
+ type="number"
225
+ value={metrics.hourly_rate_euros}
226
+ onChange={(e) => setMetrics(prev => ({
227
+ ...prev,
228
+ hourly_rate_euros: parseFloat(e.target.value) || 0
229
+ }))}
230
+ className="w-16 px-2 py-1 text-2xl font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
231
+ min="0"
232
+ step="0.5"
233
+ />
234
+ <span className="text-sm text-muted-foreground">/hr</span>
235
+ </div>
236
+ ) : (
237
+ <p className="text-2xl font-bold">
238
+ €{metrics.hourly_rate_euros}<span className="text-sm font-normal text-muted-foreground ml-1">/hr</span>
239
+ </p>
240
+ )}
257
241
  </div>
258
242
 
259
243
  {/* Job portion */}
260
- <div className="flex items-start gap-3">
261
- <div className="p-2 rounded-sm bg-[var(--cyan)]/10 shrink-0">
262
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256" className="text-[var(--cyan)]">
263
- <path d="M230.92,212c-15.23-26.33-38.7-45.21-66.09-54.16a72,72,0,1,0-73.66,0C63.78,166.78,40.31,185.66,25.08,212a8,8,0,1,0,13.85,8c18.84-32.56,52.14-52,89.07-52s70.23,19.44,89.07,52a8,8,0,1,0,13.85-8ZM72,96a56,56,0,1,1,56,56A56.06,56.06,0,0,1,72,96Z"/>
264
- </svg>
265
- </div>
266
- <div className="flex-1">
267
- <label className="text-sm text-muted-foreground block mb-1">Job Portion</label>
268
- {isEditing ? (
269
- <div className="flex items-center gap-2">
270
- <input
271
- type="number"
272
- value={Math.round(metrics.fte_equivalent * 100)}
273
- onChange={(e) => setMetrics(prev => ({
274
- ...prev,
275
- fte_equivalent: (parseFloat(e.target.value) || 0) / 100
276
- }))}
277
- className="w-16 px-2 py-1 text-lg font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
278
- min="0"
279
- max="1000"
280
- step="5"
281
- />
282
- <span className="text-muted-foreground">%</span>
283
- </div>
284
- ) : (
285
- <p className="text-2xl font-bold">
286
- {Math.round(metrics.fte_equivalent * 100)} <span className="text-base font-normal text-muted-foreground">%</span>
287
- </p>
288
- )}
289
- <p className="text-xs text-muted-foreground mt-1">% of FTE ({hoursSavedPerYear}h/year)</p>
290
- </div>
244
+ <div>
245
+ <p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Job Portion</p>
246
+ {isEditing ? (
247
+ <div className="flex items-baseline gap-1">
248
+ <input
249
+ type="number"
250
+ value={Math.round(metrics.fte_equivalent * 100)}
251
+ onChange={(e) => setMetrics(prev => ({
252
+ ...prev,
253
+ fte_equivalent: (parseFloat(e.target.value) || 0) / 100
254
+ }))}
255
+ className="w-16 px-2 py-1 text-2xl font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
256
+ min="0"
257
+ max="1000"
258
+ step="5"
259
+ />
260
+ <span className="text-sm text-muted-foreground">%</span>
261
+ </div>
262
+ ) : (
263
+ <p className="text-2xl font-bold">
264
+ {Math.round(metrics.fte_equivalent * 100)}<span className="text-sm font-normal text-muted-foreground ml-1">%</span>
265
+ </p>
266
+ )}
267
+ <p className="text-xs text-muted-foreground mt-0.5">{hoursSavedPerYear}h/year</p>
291
268
  </div>
292
269
 
293
270
  {/* Net Annual Savings */}
294
- <div className="flex items-start gap-3">
295
- <div className="p-2 rounded-sm bg-[var(--cyan)]/10 shrink-0">
296
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256" className="text-[var(--cyan)]">
297
- <path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm0-144a8,8,0,0,1,8,8v4.4c14.25,3.14,24,14.43,24,30.6,0,4.42-3.58,8-8,8s-8-3.58-8-8c0-8.64-7.18-13-16-13s-16,4.36-16,13,7.18,13,16,13c17.64,0,32,11.35,32,29,0,16.17-9.75,27.46-24,30.6V192a8,8,0,0,1-16,0v-4.4c-14.25-3.14-24-14.43-24-30.6a8,8,0,0,1,16,0c0,8.64,7.18,13,16,13s16-4.36,16-13-7.18-13-16-13c-17.64,0-32-11.35-32-29,0-16.17,9.75-27.46,24-30.6V80A8,8,0,0,1,128,72Z"/>
298
- </svg>
299
- </div>
300
- <div className="flex-1">
301
- <label className="text-sm text-muted-foreground block mb-1">Net Annual Savings</label>
302
- <p className={cn("text-2xl font-bold", netAnnualSavings >= 0 ? "text-[var(--cyan)]" : "text-red-500")}>
303
- €{netAnnualSavings.toLocaleString(undefined, { maximumFractionDigits: 0 })}
304
- </p>
305
- <p className="text-xs text-muted-foreground mt-1">
306
- €{laborSavingsPerYear.toLocaleString(undefined, { maximumFractionDigits: 0 })} labor − €{workerCostPerYear} worker
307
- </p>
308
- </div>
271
+ <div>
272
+ <p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Net Annual Savings</p>
273
+ <p className={cn("text-2xl font-bold", netAnnualSavings >= 0 ? "text-[var(--cyan)]" : "text-red-500")}>
274
+ €{netAnnualSavings.toLocaleString(undefined, { maximumFractionDigits: 0 })}
275
+ </p>
276
+ <p className="text-xs text-muted-foreground mt-0.5">
277
+ €{laborSavingsPerYear.toLocaleString(undefined, { maximumFractionDigits: 0 })} − €{workerCostPerYear}
278
+ </p>
309
279
  </div>
310
280
  </div>
281
+
282
+ {/* Implied frequency indicator */}
283
+ {impliedFrequencyPerYear > 0 && (
284
+ <p className="text-xs text-muted-foreground mt-4 pt-3 border-t border-border/50">
285
+ Implied: ~{impliedFrequencyPerMonth}×/month ({impliedFrequencyPerYear}×/year)
286
+ </p>
287
+ )}
311
288
  </CardContent>
312
289
  </Card>
313
290
  )