@yuzu-jobs/ui-components 0.1.0 → 0.2.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/index.ts +16 -0
- package/package.json +1 -1
- package/src/BackLink.astro +18 -0
- package/src/DashboardSidebarNavItem.astro +33 -0
- package/src/InputChips.astro +73 -0
- package/src/InputRadio.astro +40 -0
- package/src/InputTextarea.astro +29 -0
- package/src/JobSalary.astro +47 -0
- package/src/PageTabs.astro +43 -0
- package/src/Pagination.astro +106 -0
package/index.ts
CHANGED
|
@@ -6,11 +6,19 @@ import InputSelect from "./src/InputSelect.astro";
|
|
|
6
6
|
import InputSwitch from "./src/InputSwitch.astro";
|
|
7
7
|
import InputContainer from "./src/InputContainer.astro";
|
|
8
8
|
import InputLabel from "./src/InputLabel.astro";
|
|
9
|
+
import InputChips from "./src/InputChips.astro";
|
|
10
|
+
import InputRadio from "./src/InputRadio.astro";
|
|
11
|
+
import InputTextarea from "./src/InputTextarea.astro";
|
|
9
12
|
import Alert from "./src/Alert.astro";
|
|
10
13
|
import ProgressBar from "./src/ProgressBar.astro";
|
|
11
14
|
import Button from "./src/Button.astro";
|
|
12
15
|
import ProfileSetupFlowHeader from "./src/ProfileSetupFlowHeader.astro";
|
|
13
16
|
import StatusDot from "./src/StatusDot.astro";
|
|
17
|
+
import DashboardSidebarNavItem from "./src/DashboardSidebarNavItem.astro";
|
|
18
|
+
import PageTabs from "./src/PageTabs.astro";
|
|
19
|
+
import Pagination from "./src/Pagination.astro";
|
|
20
|
+
import BackLink from "./src/BackLink.astro";
|
|
21
|
+
import JobSalary from "./src/JobSalary.astro";
|
|
14
22
|
|
|
15
23
|
export {
|
|
16
24
|
Button,
|
|
@@ -20,7 +28,15 @@ export {
|
|
|
20
28
|
InputSwitch,
|
|
21
29
|
InputContainer,
|
|
22
30
|
InputLabel,
|
|
31
|
+
InputChips,
|
|
32
|
+
InputRadio,
|
|
33
|
+
InputTextarea,
|
|
23
34
|
Alert,
|
|
24
35
|
ProgressBar,
|
|
25
36
|
ProfileSetupFlowHeader,
|
|
37
|
+
DashboardSidebarNavItem,
|
|
38
|
+
PageTabs,
|
|
39
|
+
Pagination,
|
|
40
|
+
BackLink,
|
|
41
|
+
JobSalary,
|
|
26
42
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ArrowLeft } from "@lucide/astro";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
href: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { href, label } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<a
|
|
13
|
+
href={href}
|
|
14
|
+
class="inline-flex items-center gap-1.5 mb-2 text-sm text-slate-500 hover:text-brand-600 transition-colors group"
|
|
15
|
+
>
|
|
16
|
+
<ArrowLeft class="w-3.5 h-3.5 transition-transform group-hover:-translate-x-0.5" />
|
|
17
|
+
{label}
|
|
18
|
+
</a>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { AstroComponentFactory } from "astro/runtime/server/index.js";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
name: string;
|
|
6
|
+
href: string;
|
|
7
|
+
icon: AstroComponentFactory;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { name, href, icon: Icon, isActive } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<li class="flex">
|
|
15
|
+
<a
|
|
16
|
+
class={`relative w-full px-3 py-2.5 flex items-center gap-3 rounded-md text-sm font-sans transition-colors ${
|
|
17
|
+
isActive
|
|
18
|
+
? "bg-brand-500/15 text-white"
|
|
19
|
+
: "text-ink-300 hover:bg-white/5 hover:text-white"
|
|
20
|
+
}`}
|
|
21
|
+
href={href}
|
|
22
|
+
>
|
|
23
|
+
{
|
|
24
|
+
isActive && (
|
|
25
|
+
<span class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-brand-500" />
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
<span class={isActive ? "text-brand-400" : "text-ink-400"}>
|
|
29
|
+
<Icon size={16} />
|
|
30
|
+
</span>
|
|
31
|
+
{name}
|
|
32
|
+
</a>
|
|
33
|
+
</li>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Option {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
name: string;
|
|
9
|
+
options: Option[];
|
|
10
|
+
values?: string[];
|
|
11
|
+
required?: boolean;
|
|
12
|
+
locked?: boolean;
|
|
13
|
+
lockedValues?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
name,
|
|
18
|
+
options,
|
|
19
|
+
values = [],
|
|
20
|
+
required,
|
|
21
|
+
locked = false,
|
|
22
|
+
lockedValues = [],
|
|
23
|
+
} = Astro.props;
|
|
24
|
+
|
|
25
|
+
const activeValues = locked ? lockedValues : values;
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<div class="flex flex-wrap gap-2" data-chips-name={name}>
|
|
29
|
+
{
|
|
30
|
+
locked &&
|
|
31
|
+
lockedValues.map((value) => (
|
|
32
|
+
<input type="hidden" name={name} value={value} />
|
|
33
|
+
))
|
|
34
|
+
}
|
|
35
|
+
{
|
|
36
|
+
options.map((option) => {
|
|
37
|
+
const inputId = `${name}-${option.value}`;
|
|
38
|
+
const isChecked = activeValues.includes(option.value);
|
|
39
|
+
return (
|
|
40
|
+
<label
|
|
41
|
+
class:list={[
|
|
42
|
+
"group",
|
|
43
|
+
locked ? "cursor-not-allowed" : "cursor-pointer",
|
|
44
|
+
]}
|
|
45
|
+
for={inputId}
|
|
46
|
+
>
|
|
47
|
+
<input
|
|
48
|
+
class="sr-only peer"
|
|
49
|
+
type="checkbox"
|
|
50
|
+
id={inputId}
|
|
51
|
+
name={locked ? undefined : name}
|
|
52
|
+
value={option.value}
|
|
53
|
+
checked={isChecked}
|
|
54
|
+
disabled={locked}
|
|
55
|
+
required={!locked && required && option.value === activeValues[0]}
|
|
56
|
+
/>
|
|
57
|
+
<span
|
|
58
|
+
class:list={[
|
|
59
|
+
"inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium border transition-colors",
|
|
60
|
+
locked
|
|
61
|
+
? isChecked
|
|
62
|
+
? "bg-primary border-primary text-white opacity-50 cursor-not-allowed"
|
|
63
|
+
: "bg-white border-slate-200 text-slate-400 opacity-50 cursor-not-allowed"
|
|
64
|
+
: "bg-white border-slate-200 text-slate-600 cursor-pointer peer-checked:bg-primary peer-checked:border-primary peer-checked:text-white group-hover:border-primary group-hover:text-primary peer-checked:group-hover:text-white peer-focus-visible:ring-2 peer-focus-visible:ring-primary peer-focus-visible:ring-offset-2",
|
|
65
|
+
]}
|
|
66
|
+
>
|
|
67
|
+
{option.label}
|
|
68
|
+
</span>
|
|
69
|
+
</label>
|
|
70
|
+
);
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Option {
|
|
3
|
+
id: string;
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
name: string;
|
|
10
|
+
options: Option[];
|
|
11
|
+
required?: boolean;
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { name, options, required, defaultValue } = Astro.props;
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<div class="flex flex-col gap-2">
|
|
19
|
+
{
|
|
20
|
+
options.map((option) => (
|
|
21
|
+
<label
|
|
22
|
+
class="flex items-center gap-3 cursor-pointer group"
|
|
23
|
+
for={option.id}
|
|
24
|
+
>
|
|
25
|
+
<input
|
|
26
|
+
class="w-4 h-4 border-slate-200 accent-primary focus:ring-2 focus:ring-primary focus:ring-offset-1 cursor-pointer"
|
|
27
|
+
type="radio"
|
|
28
|
+
id={option.id}
|
|
29
|
+
name={name}
|
|
30
|
+
value={option.value}
|
|
31
|
+
required={required ?? false}
|
|
32
|
+
checked={option.value === defaultValue}
|
|
33
|
+
/>
|
|
34
|
+
<span class="text-sm group-hover:text-primary transition-colors">
|
|
35
|
+
{option.label}
|
|
36
|
+
</span>
|
|
37
|
+
</label>
|
|
38
|
+
))
|
|
39
|
+
}
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
value?: string;
|
|
8
|
+
rows?: number;
|
|
9
|
+
maxLength?: number;
|
|
10
|
+
minLength?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { id, name, placeholder, required, value, rows, maxLength, minLength } =
|
|
14
|
+
Astro.props;
|
|
15
|
+
|
|
16
|
+
const className =
|
|
17
|
+
"p-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent resize-y";
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<textarea
|
|
21
|
+
class={className}
|
|
22
|
+
id={id}
|
|
23
|
+
name={name}
|
|
24
|
+
placeholder={placeholder ?? ""}
|
|
25
|
+
required={required ?? false}
|
|
26
|
+
rows={rows ?? 4}
|
|
27
|
+
maxlength={maxLength ?? 2000}
|
|
28
|
+
minlength={minLength ?? 0}
|
|
29
|
+
>{value ?? ""}</textarea>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
type SalaryUnitType = "HOUR" | "DAY" | "WEEK" | "MONTH" | "YEAR";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
min?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
/** Accepts plain strings from loosely-typed API payloads; unknown values
|
|
8
|
+
render without a unit suffix. */
|
|
9
|
+
unitType?: SalaryUnitType | (string & {});
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { min, max, unitType, class: className } = Astro.props;
|
|
14
|
+
|
|
15
|
+
const formatSalary = (amount: number) => {
|
|
16
|
+
if (amount >= 100_000) {
|
|
17
|
+
return `$${amount / 1000}K`;
|
|
18
|
+
}
|
|
19
|
+
return new Intl.NumberFormat("en-US", {
|
|
20
|
+
style: "currency",
|
|
21
|
+
currency: "USD",
|
|
22
|
+
minimumFractionDigits: amount % 1 === 0 ? 0 : 2,
|
|
23
|
+
maximumFractionDigits: amount % 1 === 0 ? 0 : 2,
|
|
24
|
+
}).format(amount);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const salaryUnitLabel: Record<SalaryUnitType, string> = {
|
|
28
|
+
HOUR: "/hr",
|
|
29
|
+
DAY: "/day",
|
|
30
|
+
WEEK: "/wk",
|
|
31
|
+
MONTH: "/mo",
|
|
32
|
+
YEAR: "/yr",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const range = (() => {
|
|
36
|
+
const unit =
|
|
37
|
+
unitType && unitType in salaryUnitLabel
|
|
38
|
+
? salaryUnitLabel[unitType as SalaryUnitType]
|
|
39
|
+
: "";
|
|
40
|
+
if (min && max) return `${formatSalary(min)} – ${formatSalary(max)}${unit}`;
|
|
41
|
+
if (min) return `From ${formatSalary(min)}${unit}`;
|
|
42
|
+
if (max) return `Up to ${formatSalary(max)}${unit}`;
|
|
43
|
+
return null;
|
|
44
|
+
})();
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
{range && <span class={className}>{range}</span>}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Tab {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
/** Optional notification badge count — only shown when > 0 */
|
|
6
|
+
badge?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
tabs: Tab[];
|
|
11
|
+
/** The href of the currently active tab (exact match) */
|
|
12
|
+
activeHref: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { tabs, activeHref } = Astro.props;
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<div class="bg-white border-b border-slate-200 px-4 md:px-8">
|
|
19
|
+
<nav class="flex -mb-px">
|
|
20
|
+
{
|
|
21
|
+
tabs.map((tab) => {
|
|
22
|
+
const isActive = tab.href === activeHref;
|
|
23
|
+
return (
|
|
24
|
+
<a
|
|
25
|
+
href={tab.href}
|
|
26
|
+
class={`inline-flex items-center gap-2 px-4 py-3 text-sm font-medium font-sans border-b-2 transition-colors ${
|
|
27
|
+
isActive
|
|
28
|
+
? "border-brand-600 text-brand-700"
|
|
29
|
+
: "border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300"
|
|
30
|
+
}`}
|
|
31
|
+
>
|
|
32
|
+
{tab.label}
|
|
33
|
+
{tab.badge !== undefined && tab.badge > 0 && (
|
|
34
|
+
<span class="inline-flex items-center justify-center min-w-[1.1rem] h-[1.1rem] px-1 rounded-full text-xs font-bold bg-amber-400 text-amber-900 leading-none">
|
|
35
|
+
{tab.badge}
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
</a>
|
|
39
|
+
);
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
</nav>
|
|
43
|
+
</div>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "@lucide/astro";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
currentPage: number;
|
|
6
|
+
totalPages: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { currentPage, totalPages } = Astro.props;
|
|
10
|
+
|
|
11
|
+
function pageHref(page: number): string {
|
|
12
|
+
const url = new URL(Astro.url);
|
|
13
|
+
url.searchParams.set("page", String(page));
|
|
14
|
+
return url.pathname + url.search;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getPageNumbers(current: number, total: number): (number | "...")[] {
|
|
18
|
+
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
19
|
+
|
|
20
|
+
const set = new Set(
|
|
21
|
+
[1, total, current - 1, current, current + 1].filter(
|
|
22
|
+
(p) => p >= 1 && p <= total,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
const sorted = Array.from(set).sort((a, b) => a - b);
|
|
26
|
+
|
|
27
|
+
const result: (number | "...")[] = [];
|
|
28
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
29
|
+
if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push("...");
|
|
30
|
+
result.push(sorted[i]);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pages = getPageNumbers(currentPage, totalPages);
|
|
36
|
+
const hasPrev = currentPage > 1;
|
|
37
|
+
const hasNext = currentPage < totalPages;
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<nav aria-label="Pagination" class="flex items-center justify-center gap-1">
|
|
41
|
+
<!-- Previous -->
|
|
42
|
+
{
|
|
43
|
+
hasPrev ? (
|
|
44
|
+
<a
|
|
45
|
+
href={pageHref(currentPage - 1)}
|
|
46
|
+
aria-label="Previous page"
|
|
47
|
+
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-slate-500 hover:bg-brand-50 hover:text-brand-700 transition-colors"
|
|
48
|
+
>
|
|
49
|
+
<ChevronLeft class="w-4 h-4" />
|
|
50
|
+
</a>
|
|
51
|
+
) : (
|
|
52
|
+
<span
|
|
53
|
+
aria-disabled="true"
|
|
54
|
+
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-slate-300 cursor-not-allowed"
|
|
55
|
+
>
|
|
56
|
+
<ChevronLeft class="w-4 h-4" />
|
|
57
|
+
</span>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
<!-- Page numbers -->
|
|
62
|
+
{
|
|
63
|
+
pages.map((page) =>
|
|
64
|
+
page === "..." ? (
|
|
65
|
+
<span class="inline-flex items-center justify-center w-9 h-9 text-sm text-slate-400 select-none">
|
|
66
|
+
…
|
|
67
|
+
</span>
|
|
68
|
+
) : page === currentPage ? (
|
|
69
|
+
<span
|
|
70
|
+
aria-current="page"
|
|
71
|
+
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-sm font-semibold bg-brand-600 text-white"
|
|
72
|
+
>
|
|
73
|
+
{page}
|
|
74
|
+
</span>
|
|
75
|
+
) : (
|
|
76
|
+
<a
|
|
77
|
+
href={pageHref(page)}
|
|
78
|
+
aria-label={`Page ${page}`}
|
|
79
|
+
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-sm text-slate-600 hover:bg-brand-50 hover:text-brand-700 transition-colors"
|
|
80
|
+
>
|
|
81
|
+
{page}
|
|
82
|
+
</a>
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
<!-- Next -->
|
|
88
|
+
{
|
|
89
|
+
hasNext ? (
|
|
90
|
+
<a
|
|
91
|
+
href={pageHref(currentPage + 1)}
|
|
92
|
+
aria-label="Next page"
|
|
93
|
+
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-slate-500 hover:bg-brand-50 hover:text-brand-700 transition-colors"
|
|
94
|
+
>
|
|
95
|
+
<ChevronRight class="w-4 h-4" />
|
|
96
|
+
</a>
|
|
97
|
+
) : (
|
|
98
|
+
<span
|
|
99
|
+
aria-disabled="true"
|
|
100
|
+
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-slate-300 cursor-not-allowed"
|
|
101
|
+
>
|
|
102
|
+
<ChevronRight class="w-4 h-4" />
|
|
103
|
+
</span>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
</nav>
|