create-m5kdev 0.15.2 → 0.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m5kdev",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "license": "GPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,6 +14,7 @@
14
14
  "dependencies": {
15
15
  "{{PACKAGE_SCOPE}}/shared": "workspace:*",
16
16
  "@heroui/react": "catalog:",
17
+ "@heroui/styles": "catalog:",
17
18
  "@m5kdev/commons": "catalog:",
18
19
  "@m5kdev/frontend": "catalog:",
19
20
  "@m5kdev/web-ui": "catalog:",
@@ -1,28 +1,22 @@
1
- import { HeroUIProvider } from "@heroui/react";
2
1
  import { AuthProvider } from "@m5kdev/frontend/modules/auth/components/AuthProvider";
3
2
  import { DialogProvider } from "@m5kdev/web-ui/components/DialogProvider";
4
3
  import { ThemeProvider } from "@m5kdev/web-ui/components/theme-provider";
5
4
  import { AppLoader } from "@m5kdev/web-ui/modules/app/components/AppLoader";
6
- import { useHref, useNavigate } from "react-router";
7
5
  import { Toaster } from "sonner";
8
6
  import { TrpcQueryProvider } from "@/components/TrpcQueryProvider";
9
7
  import { Router } from "./Router";
10
8
 
11
9
  export function Providers() {
12
- const navigate = useNavigate();
13
-
14
10
  return (
15
- <HeroUIProvider navigate={navigate} useHref={useHref}>
16
- <ThemeProvider defaultTheme="light" storageKey="m5kdev-theme">
17
- <AuthProvider loader={<AppLoader />}>
18
- <TrpcQueryProvider>
19
- <DialogProvider>
20
- <Router />
21
- </DialogProvider>
22
- <Toaster richColors closeButton />
23
- </TrpcQueryProvider>
24
- </AuthProvider>
25
- </ThemeProvider>
26
- </HeroUIProvider>
11
+ <ThemeProvider defaultTheme="light" storageKey="m5kdev-theme">
12
+ <AuthProvider loader={<AppLoader />}>
13
+ <TrpcQueryProvider>
14
+ <DialogProvider>
15
+ <Router />
16
+ </DialogProvider>
17
+ <Toaster richColors closeButton />
18
+ </TrpcQueryProvider>
19
+ </AuthProvider>
20
+ </ThemeProvider>
27
21
  );
28
22
  }
@@ -3,11 +3,11 @@
3
3
  @import url("https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Manrope:wght@400;500;600;700;800&display=swap");
4
4
  @import "tailwindcss";
5
5
  @import "tw-animate-css";
6
-
7
- @plugin './hero.ts';
6
+ @import "@heroui/styles";
8
7
 
9
8
  @source "./src/**/*.{ts,tsx}";
10
- @source "../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}";
9
+ @source "../../node_modules/@heroui/react/dist/**/*.{js,mjs}";
10
+ @source "../../node_modules/@heroui/styles/dist/**/*.{js,mjs}";
11
11
  @source "../../node_modules/@m5kdev/web-ui/dist/src/**/*.{js,ts,jsx,tsx}";
12
12
 
13
13
  @custom-variant dark (&:is(.dark *));
@@ -13,19 +13,14 @@ import type {
13
13
  import {
14
14
  Button,
15
15
  Card,
16
- CardBody,
17
- CardHeader,
18
16
  Chip,
19
17
  Input,
18
+ Label,
19
+ ListBox,
20
20
  Modal,
21
- ModalBody,
22
- ModalContent,
23
- ModalFooter,
24
- ModalHeader,
25
21
  Select,
26
- SelectItem,
27
22
  Skeleton,
28
- Textarea,
23
+ TextArea,
29
24
  } from "@heroui/react";
30
25
  import { useDialog } from "@m5kdev/web-ui/components/DialogProvider";
31
26
  import { type UseQueryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -45,6 +40,7 @@ import {
45
40
  startTransition,
46
41
  useDeferredValue,
47
42
  useEffect,
43
+ useId,
48
44
  useMemo,
49
45
  useState,
50
46
  } from "react";
@@ -87,6 +83,13 @@ export function PostsRoute() {
87
83
  const queryClient = useQueryClient();
88
84
  const showDialog = useDialog();
89
85
 
86
+ const editorTitleId = useId();
87
+ const editorSlugId = useId();
88
+ const editorExcerptId = useId();
89
+ const editorContentId = useId();
90
+ const searchFieldId = useId();
91
+ const statusFieldId = useId();
92
+
90
93
  const [search, setSearch] = useQueryState("search", parseAsString.withDefault(""));
91
94
  const [status, setStatus] = useQueryState<PostStatusFilter>("status", STATUS_PARSER);
92
95
  const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
@@ -266,14 +269,16 @@ export function PostsRoute() {
266
269
  <p className="mt-4 max-w-2xl text-sm leading-7 text-muted-ink">{t("posts.hero.body")}</p>
267
270
  <div className="mt-6 flex flex-wrap gap-3">
268
271
  <Button
269
- radius="full"
270
- color="primary"
271
- startContent={<PlusIcon className="h-4 w-4" />}
272
+ className="rounded-full"
273
+ variant="primary"
272
274
  onPress={openCreate}
273
275
  >
274
- {t("posts.hero.new")}
276
+ <span className="inline-flex items-center gap-2">
277
+ <PlusIcon className="h-4 w-4" />
278
+ {t("posts.hero.new")}
279
+ </span>
275
280
  </Button>
276
- <Chip radius="full" variant="flat" color="secondary">
281
+ <Chip className="rounded-full" variant="soft" color="default">
277
282
  {isFetching ? t("posts.hero.syncing") : t("posts.hero.synced")}
278
283
  </Chip>
279
284
  </div>
@@ -286,38 +291,72 @@ export function PostsRoute() {
286
291
  <StatCard label={t("posts.stats.drafts")} value={stats.drafts} accent="stone" />
287
292
  </div>
288
293
  <div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_220px]">
289
- <Input
290
- aria-label={t("posts.filters.searchLabel")}
291
- radius="lg"
292
- variant="bordered"
293
- label={t("posts.filters.searchLabel")}
294
- placeholder={t("posts.filters.searchPlaceholder")}
295
- value={search}
296
- onValueChange={(value) => {
297
- startTransition(() => {
298
- void setSearch(value || null);
299
- void setPage(1);
300
- });
301
- }}
302
- />
303
- <Select
304
- aria-label={t("posts.filters.statusLabel")}
305
- label={t("posts.filters.statusLabel")}
306
- radius="lg"
307
- variant="bordered"
308
- selectedKeys={[status]}
309
- onSelectionChange={(keys) => {
310
- const nextValue = Array.from(keys)[0] as PostStatusFilter | undefined;
311
- startTransition(() => {
312
- void setStatus(nextValue ?? "all");
313
- void setPage(1);
314
- });
315
- }}
316
- >
317
- <SelectItem key="all">{t("posts.filters.all")}</SelectItem>
318
- <SelectItem key="draft">{t("posts.filters.draft")}</SelectItem>
319
- <SelectItem key="published">{t("posts.filters.published")}</SelectItem>
320
- </Select>
294
+ <div className="grid gap-2">
295
+ <Label className="text-sm font-medium" htmlFor={searchFieldId}>
296
+ {t("posts.filters.searchLabel")}
297
+ </Label>
298
+ <Input
299
+ id={searchFieldId}
300
+ aria-label={t("posts.filters.searchLabel")}
301
+ className="rounded-lg"
302
+ variant="secondary"
303
+ placeholder={t("posts.filters.searchPlaceholder")}
304
+ value={search}
305
+ onChange={(event) => {
306
+ const value = event.target.value;
307
+ startTransition(() => {
308
+ void setSearch(value || null);
309
+ void setPage(1);
310
+ });
311
+ }}
312
+ />
313
+ </div>
314
+ <div className="grid gap-2">
315
+ <Label className="text-sm font-medium" htmlFor={statusFieldId}>
316
+ {t("posts.filters.statusLabel")}
317
+ </Label>
318
+ <Select
319
+ aria-label={t("posts.filters.statusLabel")}
320
+ className="w-full"
321
+ variant="secondary"
322
+ selectedKey={status}
323
+ onSelectionChange={(key) => {
324
+ if (key === null) {
325
+ return;
326
+ }
327
+ const nextValue = String(key) as PostStatusFilter;
328
+ startTransition(() => {
329
+ void setStatus(nextValue);
330
+ void setPage(1);
331
+ });
332
+ }}
333
+ >
334
+ <Select.Trigger id={statusFieldId} className="rounded-lg w-full">
335
+ <Select.Value />
336
+ <Select.Indicator />
337
+ </Select.Trigger>
338
+ <Select.Popover>
339
+ <ListBox>
340
+ <ListBox.Item className="text-sm" id="all" textValue={t("posts.filters.all")}>
341
+ {t("posts.filters.all")}
342
+ <ListBox.ItemIndicator />
343
+ </ListBox.Item>
344
+ <ListBox.Item className="text-sm" id="draft" textValue={t("posts.filters.draft")}>
345
+ {t("posts.filters.draft")}
346
+ <ListBox.ItemIndicator />
347
+ </ListBox.Item>
348
+ <ListBox.Item
349
+ className="text-sm"
350
+ id="published"
351
+ textValue={t("posts.filters.published")}
352
+ >
353
+ {t("posts.filters.published")}
354
+ <ListBox.ItemIndicator />
355
+ </ListBox.Item>
356
+ </ListBox>
357
+ </Select.Popover>
358
+ </Select>
359
+ </div>
321
360
  </div>
322
361
  </div>
323
362
  </section>
@@ -326,14 +365,14 @@ export function PostsRoute() {
326
365
  <section className="grid gap-4">
327
366
  {isLoading ? (
328
367
  <>
329
- <Skeleton className="h-44 rounded-[28px]" />
330
- <Skeleton className="h-44 rounded-[28px]" />
331
- <Skeleton className="h-44 rounded-[28px]" />
368
+ <Skeleton className="h-44 rounded-[28px]" animationType="pulse" />
369
+ <Skeleton className="h-44 rounded-[28px]" animationType="pulse" />
370
+ <Skeleton className="h-44 rounded-[28px]" animationType="pulse" />
332
371
  </>
333
372
  ) : rows.length === 0 ? (
334
373
  <Card className="rounded-[30px] border border-dashed border-amber-300/70 bg-panel shadow-[0_18px_40px_rgba(81,50,24,0.1)]">
335
- <CardBody className="items-start gap-4 px-6 py-10">
336
- <Chip color="secondary" variant="flat">
374
+ <Card.Content className="flex flex-col items-start gap-4 px-6 py-10">
375
+ <Chip color="default" variant="soft">
337
376
  {t("posts.empty.eyebrow")}
338
377
  </Chip>
339
378
  <div>
@@ -342,10 +381,10 @@ export function PostsRoute() {
342
381
  {t("posts.empty.body")}
343
382
  </p>
344
383
  </div>
345
- <Button color="primary" radius="full" onPress={openCreate}>
384
+ <Button className="rounded-full" variant="primary" onPress={openCreate}>
346
385
  {t("posts.empty.action")}
347
386
  </Button>
348
- </CardBody>
387
+ </Card.Content>
349
388
  </Card>
350
389
  ) : (
351
390
  rows.map((row) => {
@@ -358,21 +397,33 @@ export function PostsRoute() {
358
397
  return (
359
398
  <Card
360
399
  key={row.id}
361
- isPressable
362
- onPress={() => setSelectedPostId(row.id)}
400
+ role="button"
401
+ tabIndex={0}
402
+ onClick={(event) => {
403
+ if ((event.target as HTMLElement).closest("button")) {
404
+ return;
405
+ }
406
+ setSelectedPostId(row.id);
407
+ }}
408
+ onKeyDown={(event) => {
409
+ if (event.key === "Enter" || event.key === " ") {
410
+ event.preventDefault();
411
+ setSelectedPostId(row.id);
412
+ }
413
+ }}
363
414
  className={
364
415
  isSelected
365
- ? "rounded-[30px] border border-emerald-300 bg-emerald-950 text-emerald-50 shadow-[0_20px_44px_rgba(31,79,70,0.24)]"
366
- : "rounded-[30px] border border-white/70 bg-panel shadow-[0_18px_40px_rgba(81,50,24,0.1)]"
416
+ ? "cursor-pointer rounded-[30px] border border-emerald-300 bg-emerald-950 text-emerald-50 shadow-[0_20px_44px_rgba(31,79,70,0.24)]"
417
+ : "cursor-pointer rounded-[30px] border border-white/70 bg-panel shadow-[0_18px_40px_rgba(81,50,24,0.1)]"
367
418
  }
368
419
  >
369
- <CardHeader className="flex items-start justify-between gap-4 px-5 pt-5">
420
+ <Card.Header className="flex items-start justify-between gap-4 px-5 pt-5">
370
421
  <div className="space-y-3">
371
422
  <div className="flex flex-wrap items-center gap-2">
372
423
  <Chip
373
424
  size="sm"
374
- color={row.status === "published" ? "success" : "secondary"}
375
- variant={isSelected ? "solid" : "flat"}
425
+ color={row.status === "published" ? "success" : "default"}
426
+ variant={isSelected ? "primary" : "soft"}
376
427
  >
377
428
  {row.status === "published"
378
429
  ? t("posts.filters.published")
@@ -397,16 +448,13 @@ export function PostsRoute() {
397
448
  </div>
398
449
  <Button
399
450
  isIconOnly
400
- radius="full"
401
- variant={isSelected ? "solid" : "flat"}
402
- className={
403
- isSelected ? "bg-emerald-100 text-emerald-950" : "bg-white/80 text-ink"
404
- }
451
+ className={`rounded-full ${isSelected ? "bg-emerald-100 text-emerald-950" : "bg-white/80 text-ink"}`}
452
+ variant={isSelected ? "secondary" : "ghost"}
405
453
  >
406
454
  <EyeIcon className="h-4 w-4" />
407
455
  </Button>
408
- </CardHeader>
409
- <CardBody className="gap-4 px-5 pb-5">
456
+ </Card.Header>
457
+ <Card.Content className="flex flex-col gap-4 px-5 pb-5">
410
458
  <div className="flex flex-wrap items-center gap-3 text-sm">
411
459
  <span className={isSelected ? "text-emerald-100/80" : "text-muted-ink"}>
412
460
  {t("posts.meta.updated")}: {formatDate(row.updatedAt ?? row.createdAt)}
@@ -419,38 +467,41 @@ export function PostsRoute() {
419
467
  </div>
420
468
  <div className="flex flex-wrap gap-2">
421
469
  <Button
422
- radius="full"
423
- variant={isSelected ? "solid" : "flat"}
424
- className={isSelected ? "bg-emerald-100 text-emerald-950" : ""}
425
- startContent={<PencilLineIcon className="h-4 w-4" />}
470
+ className={`rounded-full ${isSelected ? "bg-emerald-100 text-emerald-950" : ""}`}
471
+ variant={isSelected ? "secondary" : "ghost"}
426
472
  onPress={() => openEdit(row)}
427
473
  >
428
- {t("posts.actions.edit")}
474
+ <span className="inline-flex items-center gap-2">
475
+ <PencilLineIcon className="h-4 w-4" />
476
+ {t("posts.actions.edit")}
477
+ </span>
429
478
  </Button>
430
479
  {row.status === "draft" ? (
431
480
  <Button
432
- radius="full"
433
- color="secondary"
434
- variant={isSelected ? "solid" : "flat"}
435
- isLoading={isPublishing}
436
- startContent={<SendHorizontalIcon className="h-4 w-4" />}
481
+ className="rounded-full"
482
+ variant="secondary"
483
+ isPending={isPublishing}
437
484
  onPress={() => void onPublish(row.id)}
438
485
  >
439
- {t("posts.actions.publish")}
486
+ <span className="inline-flex items-center gap-2">
487
+ <SendHorizontalIcon className="h-4 w-4" />
488
+ {t("posts.actions.publish")}
489
+ </span>
440
490
  </Button>
441
491
  ) : null}
442
492
  <Button
443
- radius="full"
444
- color="danger"
445
- variant="flat"
446
- isLoading={isDeleting}
447
- startContent={<Trash2Icon className="h-4 w-4" />}
493
+ className="rounded-full"
494
+ variant="danger"
495
+ isPending={isDeleting}
448
496
  onPress={() => onDelete(row.id)}
449
497
  >
450
- {t("posts.actions.delete")}
498
+ <span className="inline-flex items-center gap-2">
499
+ <Trash2Icon className="h-4 w-4" />
500
+ {t("posts.actions.delete")}
501
+ </span>
451
502
  </Button>
452
503
  </div>
453
- </CardBody>
504
+ </Card.Content>
454
505
  </Card>
455
506
  );
456
507
  })
@@ -466,30 +517,34 @@ export function PostsRoute() {
466
517
  </p>
467
518
  <div className="flex items-center gap-2">
468
519
  <Button
469
- radius="full"
470
- variant="flat"
520
+ className="rounded-full"
521
+ variant="ghost"
471
522
  isDisabled={page <= 1}
472
- startContent={<ArrowLeftIcon className="h-4 w-4" />}
473
523
  onPress={() => {
474
524
  startTransition(() => {
475
525
  void setPage(Math.max(1, page - 1));
476
526
  });
477
527
  }}
478
528
  >
479
- {t("posts.pagination.previous")}
529
+ <span className="inline-flex items-center gap-2">
530
+ <ArrowLeftIcon className="h-4 w-4" />
531
+ {t("posts.pagination.previous")}
532
+ </span>
480
533
  </Button>
481
534
  <Button
482
- radius="full"
483
- variant="flat"
535
+ className="rounded-full"
536
+ variant="ghost"
484
537
  isDisabled={page >= pageCount}
485
- endContent={<ArrowRightIcon className="h-4 w-4" />}
486
538
  onPress={() => {
487
539
  startTransition(() => {
488
540
  void setPage(Math.min(pageCount, page + 1));
489
541
  });
490
542
  }}
491
543
  >
492
- {t("posts.pagination.next")}
544
+ <span className="inline-flex items-center gap-2">
545
+ {t("posts.pagination.next")}
546
+ <ArrowRightIcon className="h-4 w-4" />
547
+ </span>
493
548
  </Button>
494
549
  </div>
495
550
  </div>
@@ -498,7 +553,7 @@ export function PostsRoute() {
498
553
 
499
554
  <aside className="sticky top-6 h-fit">
500
555
  <Card className="rounded-[30px] border border-white/70 bg-panel shadow-[0_20px_44px_rgba(81,50,24,0.1)]">
501
- <CardHeader className="items-start justify-between px-5 pt-5">
556
+ <Card.Header className="flex items-start justify-between px-5 pt-5">
502
557
  <div>
503
558
  <p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-amber-700/80">
504
559
  {t("posts.preview.eyebrow")}
@@ -509,26 +564,26 @@ export function PostsRoute() {
509
564
  </div>
510
565
  {selectedPost ? (
511
566
  <Chip
512
- color={selectedPost.status === "published" ? "success" : "secondary"}
513
- variant="flat"
567
+ color={selectedPost.status === "published" ? "success" : "default"}
568
+ variant="soft"
514
569
  >
515
570
  {selectedPost.status === "published"
516
571
  ? t("posts.filters.published")
517
572
  : t("posts.filters.draft")}
518
573
  </Chip>
519
574
  ) : null}
520
- </CardHeader>
521
- <CardBody className="gap-5 px-5 pb-5">
575
+ </Card.Header>
576
+ <Card.Content className="flex flex-col gap-5 px-5 pb-5">
522
577
  {selectedPost ? (
523
578
  <>
524
579
  <div className="rounded-[26px] border border-amber-200/70 bg-amber-50/80 p-4">
525
580
  <p className="text-sm leading-7 text-ink/80">{selectedPost.excerpt}</p>
526
581
  </div>
527
582
  <div className="flex flex-wrap gap-2">
528
- <Chip variant="flat" color="secondary">
583
+ <Chip variant="soft" color="default">
529
584
  {getReadingTime(selectedPost.content)}
530
585
  </Chip>
531
- <Chip variant="flat">
586
+ <Chip variant="soft">
532
587
  {t("posts.preview.updated")}:{" "}
533
588
  {formatDate(selectedPost.updatedAt ?? selectedPost.createdAt)}
534
589
  </Chip>
@@ -537,12 +592,14 @@ export function PostsRoute() {
537
592
  <p>{selectedPost.content}</p>
538
593
  </div>
539
594
  <Button
540
- radius="full"
541
- variant="flat"
542
- startContent={<FilePenLineIcon className="h-4 w-4" />}
595
+ className="rounded-full"
596
+ variant="ghost"
543
597
  onPress={() => openEdit(selectedPost)}
544
598
  >
545
- {t("posts.preview.openEditor")}
599
+ <span className="inline-flex items-center gap-2">
600
+ <FilePenLineIcon className="h-4 w-4" />
601
+ {t("posts.preview.openEditor")}
602
+ </span>
546
603
  </Button>
547
604
  </>
548
605
  ) : (
@@ -550,76 +607,117 @@ export function PostsRoute() {
550
607
  {t("posts.preview.emptyBody")}
551
608
  </div>
552
609
  )}
553
- </CardBody>
610
+ </Card.Content>
554
611
  </Card>
555
612
  </aside>
556
613
  </div>
557
614
 
558
615
  <Modal
559
616
  isOpen={isEditorOpen}
560
- onOpenChange={setIsEditorOpen}
561
- size="4xl"
562
- scrollBehavior="inside"
617
+ onOpenChange={(open) => {
618
+ if (!open) {
619
+ setIsEditorOpen(false);
620
+ }
621
+ }}
563
622
  >
564
- <ModalContent>
565
- <form onSubmit={onSubmit}>
566
- <ModalHeader className="flex flex-col gap-2">
567
- <p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-amber-700/80">
568
- {editorState.id ? t("posts.editor.editEyebrow") : t("posts.editor.newEyebrow")}
569
- </p>
570
- <h3 className="font-editorial text-4xl leading-none text-ink">
571
- {editorState.id ? t("posts.editor.editTitle") : t("posts.editor.newTitle")}
572
- </h3>
573
- </ModalHeader>
574
- <ModalBody className="grid gap-4 pb-2">
575
- <Input
576
- label={t("posts.editor.fields.title")}
577
- radius="lg"
578
- variant="bordered"
579
- value={editorState.title}
580
- onValueChange={(value) => setEditorState((state) => ({ ...state, title: value }))}
581
- isRequired
582
- />
583
- <Input
584
- label={t("posts.editor.fields.slug")}
585
- radius="lg"
586
- variant="bordered"
587
- value={editorState.slug}
588
- onValueChange={(value) => setEditorState((state) => ({ ...state, slug: value }))}
589
- />
590
- <Textarea
591
- label={t("posts.editor.fields.excerpt")}
592
- radius="lg"
593
- variant="bordered"
594
- minRows={3}
595
- value={editorState.excerpt}
596
- onValueChange={(value) => setEditorState((state) => ({ ...state, excerpt: value }))}
597
- />
598
- <Textarea
599
- label={t("posts.editor.fields.content")}
600
- radius="lg"
601
- variant="bordered"
602
- minRows={10}
603
- value={editorState.content}
604
- onValueChange={(value) => setEditorState((state) => ({ ...state, content: value }))}
605
- isRequired
606
- />
607
- </ModalBody>
608
- <ModalFooter>
609
- <Button radius="full" variant="light" onPress={() => setIsEditorOpen(false)}>
610
- {t("posts.editor.cancel")}
611
- </Button>
612
- <Button
613
- radius="full"
614
- color="primary"
615
- type="submit"
616
- isLoading={createMutation.isPending || updateMutation.isPending}
617
- >
618
- {editorState.id ? t("posts.editor.save") : t("posts.editor.create")}
619
- </Button>
620
- </ModalFooter>
621
- </form>
622
- </ModalContent>
623
+ <Modal.Backdrop />
624
+ <Modal.Container scroll="inside" size="lg" className="max-w-5xl">
625
+ <Modal.Dialog>
626
+ <form
627
+ className="contents"
628
+ onSubmit={onSubmit}
629
+ >
630
+ <Modal.Header className="flex flex-col gap-2 px-6 pt-6">
631
+ <p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-amber-700/80">
632
+ {editorState.id ? t("posts.editor.editEyebrow") : t("posts.editor.newEyebrow")}
633
+ </p>
634
+ <Modal.Heading className="font-editorial text-4xl leading-none text-ink">
635
+ {editorState.id ? t("posts.editor.editTitle") : t("posts.editor.newTitle")}
636
+ </Modal.Heading>
637
+ </Modal.Header>
638
+ <Modal.Body className="grid gap-4 px-6 pb-2">
639
+ <div className="grid gap-2">
640
+ <Label className="text-sm font-medium" htmlFor={editorTitleId}>
641
+ {t("posts.editor.fields.title")}
642
+ </Label>
643
+ <Input
644
+ id={editorTitleId}
645
+ className="rounded-lg"
646
+ variant="secondary"
647
+ value={editorState.title}
648
+ onChange={(event) =>
649
+ setEditorState((state) => ({ ...state, title: event.target.value }))
650
+ }
651
+ isRequired
652
+ />
653
+ </div>
654
+ <div className="grid gap-2">
655
+ <Label className="text-sm font-medium" htmlFor={editorSlugId}>
656
+ {t("posts.editor.fields.slug")}
657
+ </Label>
658
+ <Input
659
+ id={editorSlugId}
660
+ className="rounded-lg"
661
+ variant="secondary"
662
+ value={editorState.slug}
663
+ onChange={(event) =>
664
+ setEditorState((state) => ({ ...state, slug: event.target.value }))
665
+ }
666
+ />
667
+ </div>
668
+ <div className="grid gap-2">
669
+ <Label className="text-sm font-medium" htmlFor={editorExcerptId}>
670
+ {t("posts.editor.fields.excerpt")}
671
+ </Label>
672
+ <TextArea
673
+ id={editorExcerptId}
674
+ className="rounded-lg min-h-[5.5rem]"
675
+ variant="secondary"
676
+ rows={3}
677
+ value={editorState.excerpt}
678
+ onChange={(event) =>
679
+ setEditorState((state) => ({ ...state, excerpt: event.target.value }))
680
+ }
681
+ />
682
+ </div>
683
+ <div className="grid gap-2">
684
+ <Label className="text-sm font-medium" htmlFor={editorContentId}>
685
+ {t("posts.editor.fields.content")}
686
+ </Label>
687
+ <TextArea
688
+ id={editorContentId}
689
+ className="rounded-lg min-h-[12rem]"
690
+ variant="secondary"
691
+ rows={10}
692
+ value={editorState.content}
693
+ onChange={(event) =>
694
+ setEditorState((state) => ({ ...state, content: event.target.value }))
695
+ }
696
+ isRequired
697
+ />
698
+ </div>
699
+ </Modal.Body>
700
+ <Modal.Footer className="px-6 pb-6">
701
+ <Button
702
+ className="rounded-full"
703
+ variant="tertiary"
704
+ type="button"
705
+ onPress={() => setIsEditorOpen(false)}
706
+ >
707
+ {t("posts.editor.cancel")}
708
+ </Button>
709
+ <Button
710
+ className="rounded-full"
711
+ variant="primary"
712
+ type="submit"
713
+ isPending={createMutation.isPending || updateMutation.isPending}
714
+ >
715
+ {editorState.id ? t("posts.editor.save") : t("posts.editor.create")}
716
+ </Button>
717
+ </Modal.Footer>
718
+ </form>
719
+ </Modal.Dialog>
720
+ </Modal.Container>
623
721
  </Modal>
624
722
  </div>
625
723
  );
@@ -3,7 +3,8 @@ packages:
3
3
 
4
4
  catalog:
5
5
  '@biomejs/biome': 2.2.0
6
- '@heroui/react': 2.8.8
6
+ '@heroui/react': 3.0.2
7
+ '@heroui/styles': 3.0.2
7
8
  '@libsql/client': 0.17.0
8
9
  '@m5kdev/backend': 0.3.3
9
10
  '@m5kdev/commons': 0.3.3
@@ -1,99 +0,0 @@
1
- import { heroui } from "@heroui/react";
2
-
3
- export default heroui({
4
- themes: {
5
- light: {
6
- colors: {
7
- primary: {
8
- "50": "#edf7f4",
9
- "100": "#cfeadd",
10
- "200": "#adddc5",
11
- "300": "#88cfac",
12
- "400": "#61c091",
13
- "500": "#1f4f46",
14
- "600": "#19433b",
15
- "700": "#12362f",
16
- "800": "#0c2923",
17
- "900": "#051a16",
18
- DEFAULT: "#1f4f46",
19
- foreground: "#fff8ef",
20
- },
21
- secondary: {
22
- "50": "#fff4e5",
23
- "100": "#fce2bd",
24
- "200": "#f8cf95",
25
- "300": "#f4bc6d",
26
- "400": "#efaa45",
27
- "500": "#c7782d",
28
- "600": "#a16024",
29
- "700": "#7b491b",
30
- "800": "#553112",
31
- "900": "#301a09",
32
- DEFAULT: "#c7782d",
33
- foreground: "#fff9f2",
34
- },
35
- background: "#f4ede2",
36
- foreground: "#171314",
37
- content1: {
38
- DEFAULT: "#fffaf1",
39
- foreground: "#171314",
40
- },
41
- content2: {
42
- DEFAULT: "#f4ede2",
43
- foreground: "#171314",
44
- },
45
- focus: "#1f4f46",
46
- },
47
- },
48
- dark: {
49
- colors: {
50
- primary: {
51
- "50": "#e2f3ef",
52
- "100": "#beded7",
53
- "200": "#98c8bc",
54
- "300": "#71b2a1",
55
- "400": "#4b9c86",
56
- "500": "#2d8470",
57
- "600": "#236959",
58
- "700": "#194e42",
59
- "800": "#10332b",
60
- "900": "#061815",
61
- DEFAULT: "#71b2a1",
62
- foreground: "#071613",
63
- },
64
- secondary: {
65
- "50": "#fff4e5",
66
- "100": "#f6dcb0",
67
- "200": "#eec379",
68
- "300": "#e6aa42",
69
- "400": "#d88d27",
70
- "500": "#b97221",
71
- "600": "#935a1b",
72
- "700": "#6d4214",
73
- "800": "#472b0d",
74
- "900": "#231406",
75
- DEFAULT: "#e6aa42",
76
- foreground: "#211306",
77
- },
78
- background: "#12100f",
79
- foreground: "#f7f1e8",
80
- content1: {
81
- DEFAULT: "#1d1917",
82
- foreground: "#f7f1e8",
83
- },
84
- content2: {
85
- DEFAULT: "#26211f",
86
- foreground: "#f7f1e8",
87
- },
88
- focus: "#71b2a1",
89
- },
90
- },
91
- },
92
- layout: {
93
- radius: {
94
- small: "18px",
95
- medium: "24px",
96
- large: "32px",
97
- },
98
- },
99
- });