jvdm-ui 1.0.5

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -0
  3. package/bin/jvdm-ui.mjs +49 -0
  4. package/dist/atoms/index.d.ts +343 -0
  5. package/dist/atoms/index.js +3 -0
  6. package/dist/atoms/index.js.map +1 -0
  7. package/dist/chunk-AHE2576G.js +682 -0
  8. package/dist/chunk-AHE2576G.js.map +1 -0
  9. package/dist/chunk-CGDYSB4G.js +3 -0
  10. package/dist/chunk-CGDYSB4G.js.map +1 -0
  11. package/dist/chunk-JYNDKPAH.js +362 -0
  12. package/dist/chunk-JYNDKPAH.js.map +1 -0
  13. package/dist/chunk-WWOXGKLF.js +724 -0
  14. package/dist/chunk-WWOXGKLF.js.map +1 -0
  15. package/dist/client/code-block.d.ts +9 -0
  16. package/dist/client/code-block.js +65 -0
  17. package/dist/client/code-block.js.map +1 -0
  18. package/dist/client/confirm-dialog.d.ts +16 -0
  19. package/dist/client/confirm-dialog.js +181 -0
  20. package/dist/client/confirm-dialog.js.map +1 -0
  21. package/dist/client/date-picker.d.ts +10 -0
  22. package/dist/client/date-picker.js +236 -0
  23. package/dist/client/date-picker.js.map +1 -0
  24. package/dist/client/file-upload.d.ts +12 -0
  25. package/dist/client/file-upload.js +58 -0
  26. package/dist/client/file-upload.js.map +1 -0
  27. package/dist/client/gallery.d.ts +14 -0
  28. package/dist/client/gallery.js +122 -0
  29. package/dist/client/gallery.js.map +1 -0
  30. package/dist/client/menu.d.ts +24 -0
  31. package/dist/client/menu.js +115 -0
  32. package/dist/client/menu.js.map +1 -0
  33. package/dist/client/modal.d.ts +19 -0
  34. package/dist/client/modal.js +147 -0
  35. package/dist/client/modal.js.map +1 -0
  36. package/dist/client/multi-select.d.ts +16 -0
  37. package/dist/client/multi-select.js +84 -0
  38. package/dist/client/multi-select.js.map +1 -0
  39. package/dist/client/number-input.d.ts +6 -0
  40. package/dist/client/number-input.js +59 -0
  41. package/dist/client/number-input.js.map +1 -0
  42. package/dist/client/password-input.d.ts +16 -0
  43. package/dist/client/password-input.js +103 -0
  44. package/dist/client/password-input.js.map +1 -0
  45. package/dist/client/pin-input.d.ts +10 -0
  46. package/dist/client/pin-input.js +55 -0
  47. package/dist/client/pin-input.js.map +1 -0
  48. package/dist/client/theme-toggle.d.ts +8 -0
  49. package/dist/client/theme-toggle.js +126 -0
  50. package/dist/client/theme-toggle.js.map +1 -0
  51. package/dist/client/theme.d.ts +8 -0
  52. package/dist/client/theme.js +32 -0
  53. package/dist/client/theme.js.map +1 -0
  54. package/dist/client/time-picker.d.ts +10 -0
  55. package/dist/client/time-picker.js +140 -0
  56. package/dist/client/time-picker.js.map +1 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +6 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/molecules/index.d.ts +309 -0
  61. package/dist/molecules/index.js +4 -0
  62. package/dist/molecules/index.js.map +1 -0
  63. package/dist/organisms/index.d.ts +113 -0
  64. package/dist/organisms/index.js +4 -0
  65. package/dist/organisms/index.js.map +1 -0
  66. package/dist/presets/gavel.css +19 -0
  67. package/dist/theme/index.d.ts +60 -0
  68. package/dist/theme/index.js +85 -0
  69. package/dist/theme/index.js.map +1 -0
  70. package/dist/tokens/index.d.ts +8 -0
  71. package/dist/tokens/index.js +3 -0
  72. package/dist/tokens/index.js.map +1 -0
  73. package/dist/tokens.css +129 -0
  74. package/package.json +114 -0
@@ -0,0 +1,236 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/atoms/date-picker/index.tsx
5
+ import { useEffect, useRef, useState } from "react";
6
+
7
+ // src/atoms/icon/svg/index.tsx
8
+ import { jsx } from "react/jsx-runtime";
9
+ var SIZES = {
10
+ sm: "h-3 w-3",
11
+ md: "h-4 w-4",
12
+ lg: "h-5 w-5",
13
+ xl: "h-6 w-6",
14
+ "2xl": "h-8 w-8"
15
+ };
16
+ function Svg({ size = "md", className = "", children }) {
17
+ return /* @__PURE__ */ jsx(
18
+ "svg",
19
+ {
20
+ className: `${SIZES[size]} shrink-0 ${className}`,
21
+ viewBox: "0 0 24 24",
22
+ fill: "none",
23
+ stroke: "currentColor",
24
+ strokeWidth: "1.6",
25
+ strokeLinecap: "round",
26
+ strokeLinejoin: "round",
27
+ "aria-hidden": "true",
28
+ children
29
+ }
30
+ );
31
+ }
32
+
33
+ // src/atoms/icon/chevron-icon/index.tsx
34
+ import { jsx as jsx2 } from "react/jsx-runtime";
35
+ function ChevronIcon(p) {
36
+ return /* @__PURE__ */ jsx2(Svg, { ...p, children: /* @__PURE__ */ jsx2("path", { d: "m6 9 6 6 6-6" }) });
37
+ }
38
+
39
+ // src/atoms/date-picker/locales.ts
40
+ var locales = {
41
+ previous: "Previous month",
42
+ next: "Next month",
43
+ calendar: "Choose a date",
44
+ today: "Today"
45
+ };
46
+
47
+ // src/atoms/date-picker/index.tsx
48
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
49
+ var CONTROL = "w-full min-w-0 rounded-md border border-line-strong bg-field px-4 py-3 text-base text-ink transition-[background-color,border-color,box-shadow] duration-(--duration-fast) ease-out outline-none focus:border-accent focus:ring-2 focus:ring-accent/30 sm:text-sm";
50
+ var WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
51
+ var MONTHS = Array.from(
52
+ { length: 12 },
53
+ (_, index) => new Intl.DateTimeFormat("en-US", { month: "short" }).format(new Date(2020, index, 1))
54
+ );
55
+ function dateKey(date) {
56
+ return [
57
+ date.getFullYear(),
58
+ String(date.getMonth() + 1).padStart(2, "0"),
59
+ String(date.getDate()).padStart(2, "0")
60
+ ].join("-");
61
+ }
62
+ function parseDate(value) {
63
+ if (!value) return /* @__PURE__ */ new Date();
64
+ const [year, month, day] = value.split("-").map(Number);
65
+ return year && month && day ? new Date(year, month - 1, day) : /* @__PURE__ */ new Date();
66
+ }
67
+ function formatDate(value) {
68
+ return new Intl.DateTimeFormat("en-US", {
69
+ day: "2-digit",
70
+ month: "short",
71
+ year: "numeric"
72
+ }).format(parseDate(value));
73
+ }
74
+ function DatePicker({
75
+ value,
76
+ defaultValue = "",
77
+ onValueChange,
78
+ className = "",
79
+ ...props
80
+ }) {
81
+ const initial = value ?? defaultValue;
82
+ const [selected, setSelected] = useState(initial);
83
+ const [month, setMonth] = useState(() => {
84
+ const date = parseDate(initial);
85
+ return new Date(date.getFullYear(), date.getMonth(), 1);
86
+ });
87
+ const [open, setOpen] = useState(false);
88
+ const [view, setView] = useState("calendar");
89
+ const ref = useRef(null);
90
+ useEffect(() => {
91
+ function handleDocument(event) {
92
+ if (!ref.current?.contains(event.target)) setOpen(false);
93
+ }
94
+ function handleKey(event) {
95
+ if (event.key === "Escape") setOpen(false);
96
+ }
97
+ document.addEventListener("mousedown", handleDocument);
98
+ document.addEventListener("keydown", handleKey);
99
+ return () => {
100
+ document.removeEventListener("mousedown", handleDocument);
101
+ document.removeEventListener("keydown", handleKey);
102
+ };
103
+ }, []);
104
+ const days = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
105
+ const offset = new Date(month.getFullYear(), month.getMonth(), 1).getDay();
106
+ const monthLabel = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(
107
+ month
108
+ );
109
+ function choose(date) {
110
+ const next = dateKey(date);
111
+ setSelected(next);
112
+ onValueChange?.(next);
113
+ setOpen(false);
114
+ setView("calendar");
115
+ }
116
+ return /* @__PURE__ */ jsxs(
117
+ "div",
118
+ {
119
+ className: `date-picker-root relative w-full ${open ? "date-picker-root-open" : ""}`,
120
+ ref,
121
+ children: [
122
+ /* @__PURE__ */ jsx3(
123
+ "input",
124
+ {
125
+ ...props,
126
+ className: `${CONTROL} cursor-pointer ${className}`,
127
+ onClick: () => setOpen(true),
128
+ readOnly: true,
129
+ value: selected ? formatDate(selected) : ""
130
+ }
131
+ ),
132
+ /* @__PURE__ */ jsx3(
133
+ ChevronIcon,
134
+ {
135
+ className: "pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 text-ink-muted",
136
+ size: "md"
137
+ }
138
+ ),
139
+ open ? /* @__PURE__ */ jsxs("div", { className: "absolute top-full right-0 z-20 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-lg border border-line bg-surface p-4 text-ink shadow-popover", children: [
140
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
141
+ /* @__PURE__ */ jsx3(
142
+ "button",
143
+ {
144
+ "aria-label": locales.previous,
145
+ className: "flex size-8 items-center justify-center rounded-sm text-ink-muted transition-colors duration-(--duration-fast) ease-out hover:bg-raised hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
146
+ onClick: () => setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1)),
147
+ type: "button",
148
+ children: /* @__PURE__ */ jsx3(ChevronIcon, { className: "rotate-90", size: "sm" })
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsx3(
152
+ "button",
153
+ {
154
+ className: "rounded-sm px-2 py-1 text-sm font-medium transition-colors duration-(--duration-fast) ease-out hover:bg-raised focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
155
+ onClick: () => setView(view === "calendar" ? "months" : view === "months" ? "years" : "calendar"),
156
+ type: "button",
157
+ children: monthLabel
158
+ }
159
+ ),
160
+ /* @__PURE__ */ jsx3(
161
+ "button",
162
+ {
163
+ "aria-label": locales.next,
164
+ className: "flex size-8 items-center justify-center rounded-sm text-ink-muted transition-colors duration-(--duration-fast) ease-out hover:bg-raised hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
165
+ onClick: () => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1)),
166
+ type: "button",
167
+ children: /* @__PURE__ */ jsx3(ChevronIcon, { className: "-rotate-90", size: "sm" })
168
+ }
169
+ )
170
+ ] }),
171
+ view === "years" ? /* @__PURE__ */ jsx3("div", { className: "mt-4 grid grid-cols-3 gap-2", children: Array.from({ length: 12 }, (_, index) => month.getFullYear() - 5 + index).map(
172
+ (year) => /* @__PURE__ */ jsx3(
173
+ "button",
174
+ {
175
+ className: `rounded-sm px-2 py-3 text-sm transition-colors duration-(--duration-fast) ease-out hover:bg-raised focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${year === month.getFullYear() ? "bg-accent text-on-accent" : "text-ink-soft"}`,
176
+ onClick: () => {
177
+ setMonth(new Date(year, month.getMonth(), 1));
178
+ setView("months");
179
+ },
180
+ type: "button",
181
+ children: year
182
+ },
183
+ year
184
+ )
185
+ ) }) : null,
186
+ view === "months" ? /* @__PURE__ */ jsx3("div", { className: "mt-4 grid grid-cols-3 gap-2", children: MONTHS.map((name, index) => /* @__PURE__ */ jsx3(
187
+ "button",
188
+ {
189
+ className: `rounded-sm px-2 py-3 text-sm transition-colors duration-(--duration-fast) ease-out hover:bg-raised focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${index === month.getMonth() ? "bg-accent text-on-accent" : "text-ink-soft"}`,
190
+ onClick: () => {
191
+ setMonth(new Date(month.getFullYear(), index, 1));
192
+ setView("calendar");
193
+ },
194
+ type: "button",
195
+ children: name
196
+ },
197
+ name
198
+ )) }) : null,
199
+ view === "calendar" ? /* @__PURE__ */ jsxs("div", { "aria-label": locales.calendar, className: "mt-4 grid grid-cols-7 gap-1", role: "grid", children: [
200
+ WEEKDAYS.map((day) => /* @__PURE__ */ jsx3("span", { className: "py-1 text-center text-2xs text-ink-dim", children: day }, day)),
201
+ Array.from({ length: offset }, (_, index) => /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", className: "size-8" }, `empty-${index}`)),
202
+ Array.from({ length: days }, (_, index) => {
203
+ const date = new Date(month.getFullYear(), month.getMonth(), index + 1);
204
+ const active = dateKey(date) === selected;
205
+ return /* @__PURE__ */ jsx3(
206
+ "button",
207
+ {
208
+ "aria-current": active ? "date" : void 0,
209
+ "aria-label": date.toLocaleDateString("en-US", { dateStyle: "long" }),
210
+ className: `flex size-8 items-center justify-center rounded-sm text-xs transition-colors duration-(--duration-fast) ease-out hover:bg-raised focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${active ? "bg-accent font-medium text-on-accent hover:bg-accent-ink" : "text-ink-soft"}`,
211
+ onClick: () => choose(date),
212
+ type: "button",
213
+ children: index + 1
214
+ },
215
+ dateKey(date)
216
+ );
217
+ })
218
+ ] }) : null,
219
+ /* @__PURE__ */ jsx3(
220
+ "button",
221
+ {
222
+ className: "mt-4 w-full rounded-sm border-t border-line pt-3 text-center text-xs font-medium text-accent-ink transition-colors duration-(--duration-fast) ease-out hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
223
+ onClick: () => choose(/* @__PURE__ */ new Date()),
224
+ type: "button",
225
+ children: locales.today
226
+ }
227
+ )
228
+ ] }) : null
229
+ ]
230
+ }
231
+ );
232
+ }
233
+ export {
234
+ DatePicker
235
+ };
236
+ //# sourceMappingURL=date-picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/atoms/date-picker/index.tsx","../../src/atoms/icon/svg/index.tsx","../../src/atoms/icon/chevron-icon/index.tsx","../../src/atoms/date-picker/locales.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState, type ComponentProps } from \"react\";\n\nimport { ChevronIcon } from \"../icon\";\nimport { locales } from \"./locales\";\n\nconst CONTROL =\n \"w-full min-w-0 rounded-md border border-line-strong bg-field px-4 py-3 text-base text-ink transition-[background-color,border-color,box-shadow] duration-(--duration-fast) ease-out outline-none focus:border-accent focus:ring-2 focus:ring-accent/30 sm:text-sm\";\nconst WEEKDAYS = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"];\nconst MONTHS = Array.from({ length: 12 }, (_, index) =>\n new Intl.DateTimeFormat(\"en-US\", { month: \"short\" }).format(new Date(2020, index, 1)),\n);\n\nfunction dateKey(date: Date) {\n return [\n date.getFullYear(),\n String(date.getMonth() + 1).padStart(2, \"0\"),\n String(date.getDate()).padStart(2, \"0\"),\n ].join(\"-\");\n}\n\nfunction parseDate(value?: string) {\n if (!value) return new Date();\n const [year, month, day] = value.split(\"-\").map(Number);\n return year && month && day ? new Date(year, month - 1, day) : new Date();\n}\n\nfunction formatDate(value: string) {\n return new Intl.DateTimeFormat(\"en-US\", {\n day: \"2-digit\",\n month: \"short\",\n year: \"numeric\",\n }).format(parseDate(value));\n}\n\nexport function DatePicker({\n value,\n defaultValue = \"\",\n onValueChange,\n className = \"\",\n ...props\n}: Omit<ComponentProps<\"input\">, \"type\" | \"value\" | \"defaultValue\" | \"onChange\"> & {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n}) {\n const initial = value ?? defaultValue;\n const [selected, setSelected] = useState(initial);\n const [month, setMonth] = useState(() => {\n const date = parseDate(initial);\n return new Date(date.getFullYear(), date.getMonth(), 1);\n });\n const [open, setOpen] = useState(false);\n const [view, setView] = useState<\"calendar\" | \"months\" | \"years\">(\"calendar\");\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n function handleDocument(event: MouseEvent) {\n if (!ref.current?.contains(event.target as Node)) setOpen(false);\n }\n function handleKey(event: KeyboardEvent) {\n if (event.key === \"Escape\") setOpen(false);\n }\n document.addEventListener(\"mousedown\", handleDocument);\n document.addEventListener(\"keydown\", handleKey);\n return () => {\n document.removeEventListener(\"mousedown\", handleDocument);\n document.removeEventListener(\"keydown\", handleKey);\n };\n }, []);\n\n const days = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();\n const offset = new Date(month.getFullYear(), month.getMonth(), 1).getDay();\n const monthLabel = new Intl.DateTimeFormat(\"en-US\", { month: \"long\", year: \"numeric\" }).format(\n month,\n );\n\n function choose(date: Date) {\n const next = dateKey(date);\n setSelected(next);\n onValueChange?.(next);\n setOpen(false);\n setView(\"calendar\");\n }\n\n return (\n <div\n className={`date-picker-root relative w-full ${open ? \"date-picker-root-open\" : \"\"}`}\n ref={ref}\n >\n <input\n {...props}\n className={`${CONTROL} cursor-pointer ${className}`}\n onClick={() => setOpen(true)}\n readOnly\n value={selected ? formatDate(selected) : \"\"}\n />\n <ChevronIcon\n className=\"pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 text-ink-muted\"\n size=\"md\"\n />\n {open ? (\n <div className=\"absolute top-full right-0 z-20 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-lg border border-line bg-surface p-4 text-ink shadow-popover\">\n <div className=\"flex items-center justify-between\">\n <button\n aria-label={locales.previous}\n className=\"flex size-8 items-center justify-center rounded-sm text-ink-muted transition-colors duration-(--duration-fast) ease-out hover:bg-raised hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\"\n onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1))}\n type=\"button\"\n >\n <ChevronIcon className=\"rotate-90\" size=\"sm\" />\n </button>\n <button\n className=\"rounded-sm px-2 py-1 text-sm font-medium transition-colors duration-(--duration-fast) ease-out hover:bg-raised focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\"\n onClick={() =>\n setView(view === \"calendar\" ? \"months\" : view === \"months\" ? \"years\" : \"calendar\")\n }\n type=\"button\"\n >\n {monthLabel}\n </button>\n <button\n aria-label={locales.next}\n className=\"flex size-8 items-center justify-center rounded-sm text-ink-muted transition-colors duration-(--duration-fast) ease-out hover:bg-raised hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\"\n onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1))}\n type=\"button\"\n >\n <ChevronIcon className=\"-rotate-90\" size=\"sm\" />\n </button>\n </div>\n {view === \"years\" ? (\n <div className=\"mt-4 grid grid-cols-3 gap-2\">\n {Array.from({ length: 12 }, (_, index) => month.getFullYear() - 5 + index).map(\n (year) => (\n <button\n className={`rounded-sm px-2 py-3 text-sm transition-colors duration-(--duration-fast) ease-out hover:bg-raised focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${year === month.getFullYear() ? \"bg-accent text-on-accent\" : \"text-ink-soft\"}`}\n key={year}\n onClick={() => {\n setMonth(new Date(year, month.getMonth(), 1));\n setView(\"months\");\n }}\n type=\"button\"\n >\n {year}\n </button>\n ),\n )}\n </div>\n ) : null}\n {view === \"months\" ? (\n <div className=\"mt-4 grid grid-cols-3 gap-2\">\n {MONTHS.map((name, index) => (\n <button\n className={`rounded-sm px-2 py-3 text-sm transition-colors duration-(--duration-fast) ease-out hover:bg-raised focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${index === month.getMonth() ? \"bg-accent text-on-accent\" : \"text-ink-soft\"}`}\n key={name}\n onClick={() => {\n setMonth(new Date(month.getFullYear(), index, 1));\n setView(\"calendar\");\n }}\n type=\"button\"\n >\n {name}\n </button>\n ))}\n </div>\n ) : null}\n {view === \"calendar\" ? (\n <div aria-label={locales.calendar} className=\"mt-4 grid grid-cols-7 gap-1\" role=\"grid\">\n {WEEKDAYS.map((day) => (\n <span className=\"py-1 text-center text-2xs text-ink-dim\" key={day}>\n {day}\n </span>\n ))}\n {Array.from({ length: offset }, (_, index) => (\n <span aria-hidden=\"true\" className=\"size-8\" key={`empty-${index}`} />\n ))}\n {Array.from({ length: days }, (_, index) => {\n const date = new Date(month.getFullYear(), month.getMonth(), index + 1);\n const active = dateKey(date) === selected;\n return (\n <button\n aria-current={active ? \"date\" : undefined}\n aria-label={date.toLocaleDateString(\"en-US\", { dateStyle: \"long\" })}\n className={`flex size-8 items-center justify-center rounded-sm text-xs transition-colors duration-(--duration-fast) ease-out hover:bg-raised focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${active ? \"bg-accent font-medium text-on-accent hover:bg-accent-ink\" : \"text-ink-soft\"}`}\n key={dateKey(date)}\n onClick={() => choose(date)}\n type=\"button\"\n >\n {index + 1}\n </button>\n );\n })}\n </div>\n ) : null}\n <button\n className=\"mt-4 w-full rounded-sm border-t border-line pt-3 text-center text-xs font-medium text-accent-ink transition-colors duration-(--duration-fast) ease-out hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\"\n onClick={() => choose(new Date())}\n type=\"button\"\n >\n {locales.today}\n </button>\n </div>\n ) : null}\n </div>\n );\n}\n","import type { ReactNode } from \"react\";\n\nexport type IconSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\";\n\nconst SIZES: Record<IconSize, string> = {\n sm: \"h-3 w-3\",\n md: \"h-4 w-4\",\n lg: \"h-5 w-5\",\n xl: \"h-6 w-6\",\n \"2xl\": \"h-8 w-8\",\n};\n\nexport type Props = { size?: IconSize; className?: string };\n\nexport function Svg({ size = \"md\", className = \"\", children }: Props & { children: ReactNode }) {\n return (\n <svg\n className={`${SIZES[size]} shrink-0 ${className}`}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n {children}\n </svg>\n );\n}\n","import { Svg, type Props } from \"../svg\";\n\nexport function ChevronIcon(p: Props) {\n return (\n <Svg {...p}>\n <path d=\"m6 9 6 6 6-6\" />\n </Svg>\n );\n}\n","export const locales = {\n previous: \"Previous month\",\n next: \"Next month\",\n calendar: \"Choose a date\",\n today: \"Today\",\n};\n"],"mappings":"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","names":["jsx","jsx"]}
@@ -0,0 +1,12 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare function FileUpload({ accept, multiple, onFilesChange, children, className, }: {
5
+ accept?: string;
6
+ multiple?: boolean;
7
+ onFilesChange?: (files: FileList | null) => void;
8
+ children?: ReactNode;
9
+ className?: string;
10
+ }): react.JSX.Element;
11
+
12
+ export { FileUpload };
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/atoms/file-upload/index.tsx
5
+ import { useState } from "react";
6
+
7
+ // src/atoms/file-upload/locales.ts
8
+ var locales = { choose: "Choose files" };
9
+
10
+ // src/atoms/file-upload/index.tsx
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ function FileUpload({
13
+ accept,
14
+ multiple = false,
15
+ onFilesChange,
16
+ children = locales.choose,
17
+ className = ""
18
+ }) {
19
+ const [dragging, setDragging] = useState(false);
20
+ function handleChange(event) {
21
+ onFilesChange?.(event.target.files);
22
+ }
23
+ function handleDrop(event) {
24
+ event.preventDefault();
25
+ setDragging(false);
26
+ onFilesChange?.(event.dataTransfer.files);
27
+ }
28
+ return /* @__PURE__ */ jsxs(
29
+ "label",
30
+ {
31
+ className: `flex min-h-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-line-strong px-4 py-6 text-sm text-ink-soft transition-[background-color,border-color] duration-(--duration-fast) ease-out focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-accent hover:bg-raised ${dragging ? "border-accent bg-accent-soft" : ""} ${className}`,
32
+ onDragEnter: (event) => {
33
+ event.preventDefault();
34
+ setDragging(true);
35
+ },
36
+ onDragLeave: () => setDragging(false),
37
+ onDragOver: (event) => event.preventDefault(),
38
+ onDrop: handleDrop,
39
+ children: [
40
+ children,
41
+ /* @__PURE__ */ jsx(
42
+ "input",
43
+ {
44
+ accept,
45
+ className: "sr-only",
46
+ multiple,
47
+ onChange: handleChange,
48
+ type: "file"
49
+ }
50
+ )
51
+ ]
52
+ }
53
+ );
54
+ }
55
+ export {
56
+ FileUpload
57
+ };
58
+ //# sourceMappingURL=file-upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/atoms/file-upload/index.tsx","../../src/atoms/file-upload/locales.ts"],"sourcesContent":["\"use client\";\n\nimport { useState, type ChangeEvent, type DragEvent, type ReactNode } from \"react\";\n\nimport { locales } from \"./locales\";\n\nexport function FileUpload({\n accept,\n multiple = false,\n onFilesChange,\n children = locales.choose,\n className = \"\",\n}: {\n accept?: string;\n multiple?: boolean;\n onFilesChange?: (files: FileList | null) => void;\n children?: ReactNode;\n className?: string;\n}) {\n const [dragging, setDragging] = useState(false);\n function handleChange(event: ChangeEvent<HTMLInputElement>) {\n onFilesChange?.(event.target.files);\n }\n function handleDrop(event: DragEvent<HTMLLabelElement>) {\n event.preventDefault();\n setDragging(false);\n onFilesChange?.(event.dataTransfer.files);\n }\n\n return (\n <label\n className={`flex min-h-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-line-strong px-4 py-6 text-sm text-ink-soft transition-[background-color,border-color] duration-(--duration-fast) ease-out focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-accent hover:bg-raised ${dragging ? \"border-accent bg-accent-soft\" : \"\"} ${className}`}\n onDragEnter={(event) => {\n event.preventDefault();\n setDragging(true);\n }}\n onDragLeave={() => setDragging(false)}\n onDragOver={(event) => event.preventDefault()}\n onDrop={handleDrop}\n >\n {children}\n <input\n accept={accept}\n className=\"sr-only\"\n multiple={multiple}\n onChange={handleChange}\n type=\"file\"\n />\n </label>\n );\n}\n","export const locales = { choose: \"Choose files\" };\n"],"mappings":";;;;AAEA,SAAS,gBAAkE;;;ACFpE,IAAM,UAAU,EAAE,QAAQ,eAAe;;;AD8B5C,SAWE,KAXF;AAxBG,SAAS,WAAW;AAAA,EACzB;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,WAAW,QAAQ;AAAA,EACnB,YAAY;AACd,GAMG;AACD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,WAAS,aAAa,OAAsC;AAC1D,oBAAgB,MAAM,OAAO,KAAK;AAAA,EACpC;AACA,WAAS,WAAW,OAAoC;AACtD,UAAM,eAAe;AACrB,gBAAY,KAAK;AACjB,oBAAgB,MAAM,aAAa,KAAK;AAAA,EAC1C;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,8UAA8U,WAAW,iCAAiC,EAAE,IAAI,SAAS;AAAA,MACpZ,aAAa,CAAC,UAAU;AACtB,cAAM,eAAe;AACrB,oBAAY,IAAI;AAAA,MAClB;AAAA,MACA,aAAa,MAAM,YAAY,KAAK;AAAA,MACpC,YAAY,CAAC,UAAU,MAAM,eAAe;AAAA,MAC5C,QAAQ;AAAA,MAEP;AAAA;AAAA,QACD;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,WAAU;AAAA,YACV;AAAA,YACA,UAAU;AAAA,YACV,MAAK;AAAA;AAAA,QACP;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,14 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare function Gallery({ photos, alt, fallback, photoLabel, previousLabel, nextLabel, className, }: {
5
+ photos: string[];
6
+ alt: string;
7
+ fallback?: ReactNode;
8
+ photoLabel?: (index: number, total: number) => string;
9
+ previousLabel?: string;
10
+ nextLabel?: string;
11
+ className?: string;
12
+ }): react.JSX.Element;
13
+
14
+ export { Gallery };
@@ -0,0 +1,122 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/molecules/gallery/index.tsx
5
+ import { useState } from "react";
6
+
7
+ // src/atoms/icon/svg/index.tsx
8
+ import { jsx } from "react/jsx-runtime";
9
+ var SIZES = {
10
+ sm: "h-3 w-3",
11
+ md: "h-4 w-4",
12
+ lg: "h-5 w-5",
13
+ xl: "h-6 w-6",
14
+ "2xl": "h-8 w-8"
15
+ };
16
+ function Svg({ size = "md", className = "", children }) {
17
+ return /* @__PURE__ */ jsx(
18
+ "svg",
19
+ {
20
+ className: `${SIZES[size]} shrink-0 ${className}`,
21
+ viewBox: "0 0 24 24",
22
+ fill: "none",
23
+ stroke: "currentColor",
24
+ strokeWidth: "1.6",
25
+ strokeLinecap: "round",
26
+ strokeLinejoin: "round",
27
+ "aria-hidden": "true",
28
+ children
29
+ }
30
+ );
31
+ }
32
+
33
+ // src/atoms/icon/back-icon/index.tsx
34
+ import { jsx as jsx2 } from "react/jsx-runtime";
35
+ function BackIcon(p) {
36
+ return /* @__PURE__ */ jsx2(Svg, { ...p, children: /* @__PURE__ */ jsx2("path", { d: "m14 6-6 6 6 6" }) });
37
+ }
38
+
39
+ // src/atoms/thumb/index.tsx
40
+ import { jsx as jsx3 } from "react/jsx-runtime";
41
+ function Thumb({
42
+ src,
43
+ alt,
44
+ fallback,
45
+ className = ""
46
+ }) {
47
+ return /* @__PURE__ */ jsx3(
48
+ "div",
49
+ {
50
+ className: `flex aspect-[4/3] items-center justify-center overflow-hidden rounded-md bg-raised text-ink-dim ${className}`,
51
+ children: src ? /* @__PURE__ */ jsx3("img", { src, alt, loading: "lazy", className: "h-full w-full object-cover" }) : fallback
52
+ }
53
+ );
54
+ }
55
+
56
+ // src/molecules/gallery/locales.ts
57
+ var GALLERY = {
58
+ photoLabel: (index, total) => `Photo ${index} of ${total}`,
59
+ previous: "Previous photo",
60
+ next: "Next photo"
61
+ };
62
+
63
+ // src/molecules/gallery/index.tsx
64
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
65
+ function Gallery({
66
+ photos,
67
+ alt,
68
+ fallback,
69
+ photoLabel = GALLERY.photoLabel,
70
+ previousLabel = GALLERY.previous,
71
+ nextLabel = GALLERY.next,
72
+ className = ""
73
+ }) {
74
+ const [index, setIndex] = useState(0);
75
+ const total = photos.length;
76
+ const current = Math.min(index, Math.max(0, total - 1));
77
+ const step = (delta) => setIndex((current + delta + total) % total);
78
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-2 ${className}`, children: [
79
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
80
+ /* @__PURE__ */ jsx4(Thumb, { src: photos[current] ?? null, alt, fallback }),
81
+ total > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [
82
+ /* @__PURE__ */ jsx4(
83
+ "button",
84
+ {
85
+ type: "button",
86
+ "aria-label": previousLabel,
87
+ onClick: () => step(-1),
88
+ className: "absolute top-1/2 left-2 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-app/80 text-ink backdrop-blur transition-colors duration-(--duration-fast) ease-out hover:bg-app",
89
+ children: /* @__PURE__ */ jsx4(BackIcon, { size: "sm" })
90
+ }
91
+ ),
92
+ /* @__PURE__ */ jsx4(
93
+ "button",
94
+ {
95
+ type: "button",
96
+ "aria-label": nextLabel,
97
+ onClick: () => step(1),
98
+ className: "absolute top-1/2 right-2 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-app/80 text-ink backdrop-blur transition-colors duration-(--duration-fast) ease-out hover:bg-app",
99
+ children: /* @__PURE__ */ jsx4(BackIcon, { size: "sm", className: "rotate-180" })
100
+ }
101
+ ),
102
+ /* @__PURE__ */ jsx4("span", { className: "absolute right-2 bottom-2 rounded-sm bg-app/80 px-2 py-1 text-2xs text-ink-soft backdrop-blur", children: photoLabel(current + 1, total) })
103
+ ] }) : null
104
+ ] }),
105
+ total > 1 ? /* @__PURE__ */ jsx4("div", { className: "flex touch-pan-y gap-2 overflow-x-auto overscroll-contain", children: photos.map((photo, i) => /* @__PURE__ */ jsx4(
106
+ "button",
107
+ {
108
+ type: "button",
109
+ "aria-label": photoLabel(i + 1, total),
110
+ "aria-current": i === current,
111
+ onClick: () => setIndex(i),
112
+ className: `w-20 shrink-0 overflow-hidden rounded-sm transition-opacity duration-(--duration-fast) ease-out ${i === current ? "ring-2 ring-accent" : "opacity-60 hover:opacity-100"}`,
113
+ children: /* @__PURE__ */ jsx4(Thumb, { src: photo, alt, fallback })
114
+ },
115
+ photo
116
+ )) }) : null
117
+ ] });
118
+ }
119
+ export {
120
+ Gallery
121
+ };
122
+ //# sourceMappingURL=gallery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/molecules/gallery/index.tsx","../../src/atoms/icon/svg/index.tsx","../../src/atoms/icon/back-icon/index.tsx","../../src/atoms/thumb/index.tsx","../../src/molecules/gallery/locales.ts"],"sourcesContent":["\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\n\nimport { BackIcon, Thumb } from \"../../atoms\";\nimport { GALLERY as T } from \"./locales\";\n\nexport function Gallery({\n photos,\n alt,\n fallback,\n photoLabel = T.photoLabel,\n previousLabel = T.previous,\n nextLabel = T.next,\n className = \"\",\n}: {\n photos: string[];\n alt: string;\n fallback?: ReactNode;\n photoLabel?: (index: number, total: number) => string;\n previousLabel?: string;\n nextLabel?: string;\n className?: string;\n}) {\n const [index, setIndex] = useState(0);\n\n const total = photos.length;\n const current = Math.min(index, Math.max(0, total - 1));\n const step = (delta: number) => setIndex((current + delta + total) % total);\n\n return (\n <div className={`flex flex-col gap-2 ${className}`}>\n <div className=\"relative\">\n <Thumb src={photos[current] ?? null} alt={alt} fallback={fallback} />\n\n {total > 1 ? (\n <>\n <button\n type=\"button\"\n aria-label={previousLabel}\n onClick={() => step(-1)}\n className=\"absolute top-1/2 left-2 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-app/80 text-ink backdrop-blur transition-colors duration-(--duration-fast) ease-out hover:bg-app\"\n >\n <BackIcon size=\"sm\" />\n </button>\n\n <button\n type=\"button\"\n aria-label={nextLabel}\n onClick={() => step(1)}\n className=\"absolute top-1/2 right-2 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-app/80 text-ink backdrop-blur transition-colors duration-(--duration-fast) ease-out hover:bg-app\"\n >\n <BackIcon size=\"sm\" className=\"rotate-180\" />\n </button>\n\n <span className=\"absolute right-2 bottom-2 rounded-sm bg-app/80 px-2 py-1 text-2xs text-ink-soft backdrop-blur\">\n {photoLabel(current + 1, total)}\n </span>\n </>\n ) : null}\n </div>\n\n {total > 1 ? (\n <div className=\"flex touch-pan-y gap-2 overflow-x-auto overscroll-contain\">\n {photos.map((photo, i) => (\n <button\n key={photo}\n type=\"button\"\n aria-label={photoLabel(i + 1, total)}\n aria-current={i === current}\n onClick={() => setIndex(i)}\n className={`w-20 shrink-0 overflow-hidden rounded-sm transition-opacity duration-(--duration-fast) ease-out ${\n i === current ? \"ring-2 ring-accent\" : \"opacity-60 hover:opacity-100\"\n }`}\n >\n <Thumb src={photo} alt={alt} fallback={fallback} />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n","import type { ReactNode } from \"react\";\n\nexport type IconSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\";\n\nconst SIZES: Record<IconSize, string> = {\n sm: \"h-3 w-3\",\n md: \"h-4 w-4\",\n lg: \"h-5 w-5\",\n xl: \"h-6 w-6\",\n \"2xl\": \"h-8 w-8\",\n};\n\nexport type Props = { size?: IconSize; className?: string };\n\nexport function Svg({ size = \"md\", className = \"\", children }: Props & { children: ReactNode }) {\n return (\n <svg\n className={`${SIZES[size]} shrink-0 ${className}`}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n {children}\n </svg>\n );\n}\n","import { Svg, type Props } from \"../svg\";\n\nexport function BackIcon(p: Props) {\n return (\n <Svg {...p}>\n <path d=\"m14 6-6 6 6 6\" />\n </Svg>\n );\n}\n","import type { ReactNode } from \"react\";\n\nexport function Thumb({\n src,\n alt,\n fallback,\n className = \"\",\n}: {\n src?: string | null;\n alt: string;\n fallback?: ReactNode;\n className?: string;\n}) {\n return (\n <div\n className={`flex aspect-[4/3] items-center justify-center overflow-hidden rounded-md bg-raised text-ink-dim ${className}`}\n >\n {src ? (\n <img src={src} alt={alt} loading=\"lazy\" className=\"h-full w-full object-cover\" />\n ) : (\n fallback\n )}\n </div>\n );\n}\n","export const GALLERY = {\n photoLabel: (index: number, total: number) => `Photo ${index} of ${total}`,\n previous: \"Previous photo\",\n next: \"Next photo\",\n};\n"],"mappings":"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","names":["jsx","jsx","jsx"]}
@@ -0,0 +1,24 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare const ALIGNS: {
5
+ readonly start: "left-0";
6
+ readonly end: "right-0";
7
+ };
8
+ declare const WIDTHS: {
9
+ readonly sm: "w-48";
10
+ readonly md: "w-56";
11
+ readonly lg: "w-72";
12
+ };
13
+ type MenuAlign = keyof typeof ALIGNS;
14
+ type MenuWidth = keyof typeof WIDTHS;
15
+ declare function Menu({ label, trigger, align, width, className, children, }: {
16
+ label: string;
17
+ trigger: ReactNode;
18
+ align?: MenuAlign;
19
+ width?: MenuWidth;
20
+ className?: string;
21
+ children: ReactNode;
22
+ }): react.JSX.Element;
23
+
24
+ export { Menu, type MenuAlign, type MenuWidth };
@@ -0,0 +1,115 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/molecules/menu/menu/index.tsx
5
+ import {
6
+ Children,
7
+ cloneElement,
8
+ isValidElement,
9
+ useEffect,
10
+ useRef,
11
+ useState
12
+ } from "react";
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
+ var ALIGNS = {
15
+ start: "left-0",
16
+ end: "right-0"
17
+ };
18
+ var WIDTHS = {
19
+ sm: "w-48",
20
+ md: "w-56",
21
+ lg: "w-72"
22
+ };
23
+ function Menu({
24
+ label,
25
+ trigger,
26
+ align = "end",
27
+ width = "md",
28
+ className = "",
29
+ children
30
+ }) {
31
+ const [open, setOpen] = useState(false);
32
+ const root = useRef(null);
33
+ const triggerRef = useRef(null);
34
+ const menuRef = useRef(null);
35
+ const items = Children.toArray(children).map(
36
+ (child) => isValidElement(child) ? cloneElement(child, {
37
+ role: "menuitem",
38
+ tabIndex: -1
39
+ }) : child
40
+ );
41
+ useEffect(() => {
42
+ if (!open) return;
43
+ function onPointerDown(event) {
44
+ if (!root.current?.contains(event.target)) setOpen(false);
45
+ }
46
+ document.addEventListener("pointerdown", onPointerDown);
47
+ return () => {
48
+ document.removeEventListener("pointerdown", onPointerDown);
49
+ };
50
+ }, [open]);
51
+ useEffect(() => {
52
+ if (!open) return;
53
+ menuRef.current?.querySelector('[role="menuitem"]')?.focus();
54
+ }, [open]);
55
+ function onMenuKeyDown(event) {
56
+ const menuItems = [
57
+ ...menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []
58
+ ];
59
+ const current = menuItems.indexOf(document.activeElement);
60
+ const next = (index) => menuItems[(index + menuItems.length) % menuItems.length]?.focus();
61
+ if (event.key === "Escape") {
62
+ event.preventDefault();
63
+ setOpen(false);
64
+ triggerRef.current?.focus();
65
+ return;
66
+ }
67
+ if (event.key === "ArrowDown") {
68
+ event.preventDefault();
69
+ next(current + 1);
70
+ }
71
+ if (event.key === "ArrowUp") {
72
+ event.preventDefault();
73
+ next(current - 1);
74
+ }
75
+ if (event.key === "Home") {
76
+ event.preventDefault();
77
+ menuItems[0]?.focus();
78
+ }
79
+ if (event.key === "End") {
80
+ event.preventDefault();
81
+ menuItems.at(-1)?.focus();
82
+ }
83
+ }
84
+ return /* @__PURE__ */ jsxs("div", { ref: root, className: `relative ${className}`, children: [
85
+ /* @__PURE__ */ jsx(
86
+ "button",
87
+ {
88
+ ref: triggerRef,
89
+ type: "button",
90
+ "aria-label": label,
91
+ "aria-haspopup": "menu",
92
+ "aria-expanded": open,
93
+ onClick: () => setOpen((current) => !current),
94
+ className: "flex items-center rounded-md transition-colors duration-(--duration-fast) ease-out focus-visible:ring-2 focus-visible:ring-accent/30 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
95
+ children: trigger
96
+ }
97
+ ),
98
+ open ? /* @__PURE__ */ jsx(
99
+ "div",
100
+ {
101
+ ref: menuRef,
102
+ role: "menu",
103
+ "aria-label": label,
104
+ onClick: () => setOpen(false),
105
+ onKeyDown: onMenuKeyDown,
106
+ className: `absolute top-full z-10 mt-2 flex flex-col gap-1 rounded-md border border-line-strong bg-raised p-2 ${ALIGNS[align]} ${WIDTHS[width]}`,
107
+ children: items
108
+ }
109
+ ) : null
110
+ ] });
111
+ }
112
+ export {
113
+ Menu
114
+ };
115
+ //# sourceMappingURL=menu.js.map