@syscore/ui-library 1.25.0 → 2.0.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 (30) hide show
  1. package/client/components/ui/app-bar.tsx +299 -0
  2. package/client/components/ui/banner.tsx +108 -0
  3. package/client/components/ui/bottom-navigation.tsx +223 -0
  4. package/client/components/ui/card.tsx +108 -26
  5. package/client/components/ui/date-picker.tsx +188 -0
  6. package/client/components/ui/empty-state.tsx +197 -0
  7. package/client/components/ui/file-upload.tsx +214 -0
  8. package/client/components/ui/footer.tsx +179 -0
  9. package/client/components/ui/layout.tsx +381 -0
  10. package/client/components/ui/sidebar.tsx +11 -5
  11. package/client/components/ui/stepper.tsx +148 -0
  12. package/client/components/ui/system-bar.tsx +119 -0
  13. package/client/components/ui/timeline.tsx +181 -0
  14. package/client/global.css +2304 -41
  15. package/client/ui/AppBar/app-bar.stories.tsx +280 -0
  16. package/client/ui/Banner/banner.stories.tsx +238 -0
  17. package/client/ui/BottomNavigation/bottom-navigation.stories.tsx +285 -0
  18. package/client/ui/Card.stories.tsx +285 -168
  19. package/client/ui/DatePicker/DatePicker.stories.tsx +293 -0
  20. package/client/ui/EmptyState/empty-state.stories.tsx +251 -0
  21. package/client/ui/FileUpload/FileUpload.stories.tsx +218 -0
  22. package/client/ui/Footer/footer.stories.tsx +194 -0
  23. package/client/ui/Layout.stories.tsx +1481 -0
  24. package/client/ui/Stepper/Stepper.stories.tsx +344 -0
  25. package/client/ui/SystemBar/system-bar.stories.tsx +179 -0
  26. package/client/ui/Timeline/timeline.stories.tsx +404 -0
  27. package/dist/index.cjs.js +1 -1
  28. package/dist/index.d.ts +219 -0
  29. package/dist/index.es.js +1504 -99
  30. package/package.json +1 -1
@@ -0,0 +1,293 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { DatePicker, DateRangePicker, type DateRange } from "../../components/ui/date-picker";
4
+
5
+ const meta = {
6
+ title: "UI/DatePicker",
7
+ component: DatePicker,
8
+ tags: ["autodocs"],
9
+ parameters: {
10
+ layout: "padded",
11
+ docs: {
12
+ description: {
13
+ component: [
14
+ "A text input that opens a Calendar popover when clicked. Supports single date and date range selection.",
15
+ "",
16
+ "**The difference from Calendar:**",
17
+ "- `Calendar` — always visible grid, sits on the page",
18
+ "- `DatePicker` — input field that pops open a calendar, closes after selection",
19
+ "",
20
+ "**Import:**",
21
+ "```tsx",
22
+ `import { DatePicker, DateRangePicker } from "@syscore/ui-library"`,
23
+ "```",
24
+ "",
25
+ "**Key props:**",
26
+ "",
27
+ "| Prop | Type | Default | Effect |",
28
+ "|------|------|---------|--------|",
29
+ "| `value` | `Date` | — | Controlled selected date |",
30
+ "| `onChange` | `(date?: Date) => void` | — | Called when date is picked |",
31
+ "| `placeholder` | string | `\"Select date\"` | Input placeholder text |",
32
+ "| `dateFormat` | string | `\"MMM d, yyyy\"` | Display format (date-fns) |",
33
+ "| `fromDate` | `Date` | — | Disable dates before this |",
34
+ "| `toDate` | `Date` | — | Disable dates after this |",
35
+ "| `disabled` | boolean | `false` | Disable the input |",
36
+ ].join("\n"),
37
+ },
38
+ },
39
+ },
40
+ } satisfies Meta<typeof DatePicker>;
41
+
42
+ export default meta;
43
+ type Story = StoryObj<typeof meta>;
44
+
45
+ export const Default: Story = {
46
+ parameters: {
47
+ docs: {
48
+ source: {
49
+ code: `import { DatePicker } from "@syscore/ui-library"
50
+ import { useState } from "react"
51
+
52
+ const [date, setDate] = useState<Date>()
53
+
54
+ <DatePicker
55
+ value={date}
56
+ onChange={setDate}
57
+ placeholder="Select date"
58
+ />`,
59
+ },
60
+ },
61
+ },
62
+ render: () => {
63
+ const [date, setDate] = useState<Date>();
64
+ return (
65
+ <div className="max-w-xs">
66
+ <DatePicker
67
+ value={date}
68
+ onChange={setDate}
69
+ placeholder="Select date"
70
+ />
71
+ {date && (
72
+ <p className="mt-2 text-sm text-gray-500">Selected: {date.toDateString()}</p>
73
+ )}
74
+ </div>
75
+ );
76
+ },
77
+ };
78
+
79
+ export const WithDefaultValue: Story = {
80
+ name: "With default value",
81
+ parameters: {
82
+ docs: {
83
+ source: {
84
+ code: `import { DatePicker } from "@syscore/ui-library"
85
+ import { useState } from "react"
86
+
87
+ const [date, setDate] = useState<Date>(new Date())
88
+
89
+ <DatePicker value={date} onChange={setDate} />`,
90
+ },
91
+ },
92
+ },
93
+ render: () => {
94
+ const [date, setDate] = useState<Date>(new Date());
95
+ return (
96
+ <div className="max-w-xs">
97
+ <DatePicker value={date} onChange={setDate} />
98
+ </div>
99
+ );
100
+ },
101
+ };
102
+
103
+ export const WithDateLimits: Story = {
104
+ name: "With date limits",
105
+ parameters: {
106
+ docs: {
107
+ source: {
108
+ code: `import { DatePicker } from "@syscore/ui-library"
109
+ import { useState } from "react"
110
+
111
+ // Only allow dates from today onwards
112
+ const [date, setDate] = useState<Date>()
113
+ const today = new Date()
114
+
115
+ <DatePicker
116
+ value={date}
117
+ onChange={setDate}
118
+ fromDate={today}
119
+ placeholder="Select future date"
120
+ />`,
121
+ },
122
+ },
123
+ },
124
+ render: () => {
125
+ const [date, setDate] = useState<Date>();
126
+ const today = new Date();
127
+ return (
128
+ <div className="max-w-xs">
129
+ <DatePicker
130
+ value={date}
131
+ onChange={setDate}
132
+ fromDate={today}
133
+ placeholder="Select future date"
134
+ />
135
+ <p className="mt-2 text-xs text-gray-400">Dates before today are disabled</p>
136
+ </div>
137
+ );
138
+ },
139
+ };
140
+
141
+ export const CustomFormat: Story = {
142
+ name: "Custom date format",
143
+ parameters: {
144
+ docs: {
145
+ source: {
146
+ code: `import { DatePicker } from "@syscore/ui-library"
147
+
148
+ // Uses date-fns format tokens
149
+ <DatePicker
150
+ value={date}
151
+ onChange={setDate}
152
+ dateFormat="dd/MM/yyyy"
153
+ />`,
154
+ },
155
+ },
156
+ },
157
+ render: () => {
158
+ const [date, setDate] = useState<Date>(new Date());
159
+ return (
160
+ <div className="flex flex-col gap-4 max-w-xs">
161
+ <div className="flex flex-col gap-1">
162
+ <span className="text-xs text-gray-500">MMM d, yyyy (default)</span>
163
+ <DatePicker value={date} onChange={setDate} dateFormat="MMM d, yyyy" />
164
+ </div>
165
+ <div className="flex flex-col gap-1">
166
+ <span className="text-xs text-gray-500">dd/MM/yyyy</span>
167
+ <DatePicker value={date} onChange={setDate} dateFormat="dd/MM/yyyy" />
168
+ </div>
169
+ <div className="flex flex-col gap-1">
170
+ <span className="text-xs text-gray-500">MMMM do, yyyy</span>
171
+ <DatePicker value={date} onChange={setDate} dateFormat="MMMM do, yyyy" />
172
+ </div>
173
+ </div>
174
+ );
175
+ },
176
+ };
177
+
178
+ export const Disabled: Story = {
179
+ parameters: {
180
+ docs: {
181
+ source: {
182
+ code: `import { DatePicker } from "@syscore/ui-library"
183
+
184
+ <DatePicker disabled placeholder="Not available" />`,
185
+ },
186
+ },
187
+ },
188
+ render: () => (
189
+ <div className="max-w-xs">
190
+ <DatePicker disabled placeholder="Not available" />
191
+ </div>
192
+ ),
193
+ };
194
+
195
+ export const RangePicker: Story = {
196
+ name: "Date range picker",
197
+ parameters: {
198
+ docs: {
199
+ source: {
200
+ code: `import { DateRangePicker, type DateRange } from "@syscore/ui-library"
201
+ import { useState } from "react"
202
+
203
+ const [range, setRange] = useState<DateRange>()
204
+
205
+ <DateRangePicker
206
+ value={range}
207
+ onChange={setRange}
208
+ placeholder="Select date range"
209
+ />`,
210
+ },
211
+ },
212
+ },
213
+ render: () => {
214
+ const [range, setRange] = useState<DateRange>();
215
+ return (
216
+ <div className="max-w-sm">
217
+ <DateRangePicker
218
+ value={range}
219
+ onChange={setRange}
220
+ placeholder="Select date range"
221
+ />
222
+ {range?.from && (
223
+ <p className="mt-2 text-sm text-gray-500">
224
+ {range.from.toDateString()} {range.to ? `→ ${range.to.toDateString()}` : "→ pick end date"}
225
+ </p>
226
+ )}
227
+ </div>
228
+ );
229
+ },
230
+ };
231
+
232
+ export const InForm: Story = {
233
+ name: "Inside a form",
234
+ parameters: {
235
+ docs: {
236
+ source: {
237
+ code: `import { DatePicker, DateRangePicker } from "@syscore/ui-library"
238
+ import { Stack, Text, Button } from "@syscore/ui-library"
239
+
240
+ <Stack gap={6}>
241
+ <Stack gap={2}>
242
+ <Text variant="overline-small">Payment date</Text>
243
+ <DatePicker
244
+ value={paymentDate}
245
+ onChange={setPaymentDate}
246
+ placeholder="Select date"
247
+ />
248
+ </Stack>
249
+
250
+ <Stack gap={2}>
251
+ <Text variant="overline-small">Project period</Text>
252
+ <DateRangePicker
253
+ value={period}
254
+ onChange={setPeriod}
255
+ placeholder="Start — End"
256
+ />
257
+ </Stack>
258
+
259
+ <Button variant="primary-dark" size="large">Save</Button>
260
+ </Stack>`,
261
+ },
262
+ },
263
+ },
264
+ render: () => {
265
+ const [paymentDate, setPaymentDate] = useState<Date>();
266
+ const [period, setPeriod] = useState<DateRange>();
267
+ return (
268
+ <div className="max-w-sm border border-gray-100 rounded-xl p-6">
269
+ <div className="flex flex-col gap-6">
270
+ <div className="flex flex-col gap-2">
271
+ <span className="text-xs font-semibold uppercase tracking-wider text-gray-500">Payment date</span>
272
+ <DatePicker
273
+ value={paymentDate}
274
+ onChange={setPaymentDate}
275
+ placeholder="Select date"
276
+ />
277
+ </div>
278
+ <div className="flex flex-col gap-2">
279
+ <span className="text-xs font-semibold uppercase tracking-wider text-gray-500">Project period</span>
280
+ <DateRangePicker
281
+ value={period}
282
+ onChange={setPeriod}
283
+ placeholder="Start — End"
284
+ />
285
+ </div>
286
+ <button className="w-full bg-gray-900 text-white rounded-lg py-2.5 text-sm font-medium">
287
+ Save
288
+ </button>
289
+ </div>
290
+ </div>
291
+ );
292
+ },
293
+ };
@@ -0,0 +1,251 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { EmptyState, EmptyStateAction } from "../../components/ui/empty-state";
3
+
4
+ // ─── Inline SVG icons ─────────────────────────────────────────────────────────
5
+
6
+ function IconSearch() {
7
+ return (
8
+ <svg
9
+ width="64"
10
+ height="64"
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ strokeWidth="1.25"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ aria-hidden="true"
18
+ >
19
+ <circle cx="11" cy="11" r="8" />
20
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
21
+ </svg>
22
+ );
23
+ }
24
+
25
+ function IconCheckCircle() {
26
+ return (
27
+ <svg
28
+ width="64"
29
+ height="64"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ strokeWidth="1.25"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ aria-hidden="true"
37
+ >
38
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
39
+ <polyline points="22 4 12 14.01 9 11.01" />
40
+ </svg>
41
+ );
42
+ }
43
+
44
+ function IconFileText() {
45
+ return (
46
+ <svg
47
+ width="64"
48
+ height="64"
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ stroke="currentColor"
52
+ strokeWidth="1.25"
53
+ strokeLinecap="round"
54
+ strokeLinejoin="round"
55
+ aria-hidden="true"
56
+ >
57
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
58
+ <polyline points="14 2 14 8 20 8" />
59
+ <line x1="16" y1="13" x2="8" y2="13" />
60
+ <line x1="16" y1="17" x2="8" y2="17" />
61
+ <polyline points="10 9 9 9 8 9" />
62
+ </svg>
63
+ );
64
+ }
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+
68
+ const meta = {
69
+ title: "UI/EmptyState",
70
+ component: EmptyState,
71
+ tags: ["autodocs"],
72
+ parameters: {
73
+ layout: "padded",
74
+ },
75
+ argTypes: {
76
+ headline: { control: "text" },
77
+ title: { control: "text" },
78
+ subtitle: { control: "text" },
79
+ text: { control: "text" },
80
+ image: { control: "text" },
81
+ actionText: { control: "text" },
82
+ },
83
+ } satisfies Meta<typeof EmptyState>;
84
+
85
+ export default meta;
86
+ type Story = StoryObj<typeof meta>;
87
+
88
+ // ─── Default ─────────────────────────────────────────────────────────────────
89
+ // Simplest form — headline + title + body text only, no media.
90
+
91
+ export const Default: Story = {
92
+ render: () => (
93
+ <EmptyState
94
+ headline="No Messages Yet"
95
+ title="Check back later."
96
+ text="You haven't received any messages yet. When you do, they'll appear here."
97
+ />
98
+ ),
99
+ };
100
+
101
+ // ─── Content ─────────────────────────────────────────────────────────────────
102
+ // Props: headline, title, subtitle, text.
103
+
104
+ export const Content: Story = {
105
+ render: () => (
106
+ <EmptyState
107
+ headline="Whoops, 404"
108
+ title="Page not found"
109
+ subtitle="The requested resource couldn't be located."
110
+ text="The page you were looking for does not exist. Please check the URL or go back to the home page."
111
+ />
112
+ ),
113
+ };
114
+
115
+ // ─── Media — Icon ─────────────────────────────────────────────────────────────
116
+ // Pass a React node to the `icon` prop to render it in the media area.
117
+
118
+ export const MediaIcon: Story = {
119
+ render: () => (
120
+ <EmptyState
121
+ icon={<IconSearch />}
122
+ title="We couldn't find a match."
123
+ text="Try adjusting your search terms or filters. Sometimes less specific terms or broader queries can help you find what you're looking for."
124
+ />
125
+ ),
126
+ };
127
+
128
+ // ─── Media — Image ────────────────────────────────────────────────────────────
129
+ // Pass a URL to the `image` prop for an illustration.
130
+
131
+ export const MediaImage: Story = {
132
+ render: () => (
133
+ <EmptyState
134
+ image="https://cdn.vuetifyjs.com/docs/images/components/v-empty-state/teamwork.png"
135
+ imageAlt="Two people working together"
136
+ title="Manage your inventory transfers"
137
+ text="Track and receive your incoming inventory from suppliers"
138
+ />
139
+ ),
140
+ };
141
+
142
+ // ─── Actions ─────────────────────────────────────────────────────────────────
143
+ // Use `actionText` + `onClickAction` for a single auto-rendered CTA button.
144
+
145
+ export const Actions: Story = {
146
+ render: () => (
147
+ <EmptyState
148
+ image="https://cdn.vuetifyjs.com/docs/images/components/v-empty-state/connection.svg"
149
+ imageAlt="Connection error illustration"
150
+ title="Something Went Wrong"
151
+ text="There might be a problem with your connection or our servers. Please check your internet connection or try again later. We appreciate your patience."
152
+ actionText="Retry Request"
153
+ onClickAction={() => alert("Retrying…")}
154
+ />
155
+ ),
156
+ };
157
+
158
+ // ─── Slot: Default ────────────────────────────────────────────────────────────
159
+ // Anything passed as children renders in the default slot below the text.
160
+
161
+ export const SlotDefault: Story = {
162
+ render: () => (
163
+ <EmptyState
164
+ headline="Welcome,"
165
+ icon={<IconFileText />}
166
+ title="What would you like to do today?"
167
+ >
168
+ <div
169
+ style={{
170
+ display: "grid",
171
+ gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
172
+ gap: "1rem",
173
+ marginTop: "1.5rem",
174
+ width: "100%",
175
+ }}
176
+ >
177
+ {[
178
+ { title: "Learn More", description: "Start with our dedicated feature guides" },
179
+ { title: "Try the Playground", description: "Test things out interactively" },
180
+ { title: "Create a Bin", description: "Create a new bin to store your code" },
181
+ { title: "Report a Bug", description: "File a bug report for Vuetify" },
182
+ ].map((card) => (
183
+ <div key={card.title} className="empty-state-card">
184
+ <div className="empty-state-card-title">{card.title}</div>
185
+ <div className="empty-state-card-desc">{card.description}</div>
186
+ </div>
187
+ ))}
188
+ </div>
189
+ </EmptyState>
190
+ ),
191
+ };
192
+
193
+ // ─── Slot: Media ─────────────────────────────────────────────────────────────
194
+ // `mediaSlot` fully replaces the media area.
195
+
196
+ export const SlotMedia: Story = {
197
+ render: () => (
198
+ <EmptyState
199
+ mediaSlot={
200
+ <span style={{ color: "var(--color-gray-400, #9ca3af)", display: "flex" }}>
201
+ <IconCheckCircle />
202
+ </span>
203
+ }
204
+ headlineSlot={
205
+ <div style={{ fontSize: "2rem", fontWeight: 700, color: "var(--color-gray-700, #3d3f47)" }}>
206
+ All Done For Now!
207
+ </div>
208
+ }
209
+ titleSlot={
210
+ <div style={{ fontSize: "1.25rem", fontWeight: 600 }}>
211
+ You're all caught up.
212
+ </div>
213
+ }
214
+ textSlot={
215
+ <div style={{ color: "var(--color-gray-500, #71747d)", fontSize: "0.875rem" }}>
216
+ Great job on completing all your tasks! This might be a good time to
217
+ relax or consider planning your next set of goals. If you think of
218
+ something new, just hit the button below to add a new task.
219
+ </div>
220
+ }
221
+ />
222
+ ),
223
+ };
224
+
225
+ // ─── Slot: Actions ────────────────────────────────────────────────────────────
226
+ // `actionsSlot` lets you compose multiple buttons in the actions area.
227
+
228
+ export const SlotActions: Story = {
229
+ render: () => (
230
+ <EmptyState
231
+ image="https://cdn.vuetifyjs.com/docs/images/components/v-empty-state/teamwork.png"
232
+ imageAlt="Teamwork illustration"
233
+ titleSlot={
234
+ <div style={{ fontSize: "1rem", fontWeight: 600, marginTop: "2rem" }}>
235
+ Manage your inventory transfers
236
+ </div>
237
+ }
238
+ textSlot={
239
+ <div style={{ fontSize: "0.875rem", color: "var(--color-gray-500, #71747d)" }}>
240
+ Track and receive your incoming inventory from suppliers
241
+ </div>
242
+ }
243
+ actionsSlot={
244
+ <>
245
+ <EmptyStateAction variant="outline">Learn more</EmptyStateAction>
246
+ <EmptyStateAction variant="primary">Add transfer</EmptyStateAction>
247
+ </>
248
+ }
249
+ />
250
+ ),
251
+ };