lightnet 3.10.8 → 3.12.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 (49) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/__e2e__/admin.spec.ts +35 -1
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
  5. package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
  6. package/__e2e__/fixtures/basics/package.json +5 -5
  7. package/__tests__/pages/details-page/create-content-metadata.spec.ts +23 -3
  8. package/package.json +8 -8
  9. package/src/admin/components/form/DynamicArray.tsx +81 -29
  10. package/src/admin/components/form/Input.tsx +17 -3
  11. package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +12 -5
  12. package/src/admin/components/form/MarkdownEditor.tsx +12 -4
  13. package/src/admin/components/form/Select.tsx +25 -13
  14. package/src/admin/components/form/SubmitButton.tsx +2 -2
  15. package/src/admin/components/form/atoms/Button.tsx +27 -0
  16. package/src/admin/components/form/atoms/FileUpload.tsx +124 -49
  17. package/src/admin/components/form/atoms/Hint.tsx +2 -2
  18. package/src/admin/components/form/atoms/Label.tsx +17 -6
  19. package/src/admin/components/form/utils/get-border-class.ts +22 -0
  20. package/src/admin/i18n/translations/en.yml +19 -3
  21. package/src/admin/pages/AdminRoute.astro +1 -1
  22. package/src/admin/pages/media/EditForm.tsx +33 -15
  23. package/src/admin/pages/media/EditRoute.astro +2 -2
  24. package/src/admin/pages/media/fields/Authors.tsx +15 -5
  25. package/src/admin/pages/media/fields/Categories.tsx +17 -5
  26. package/src/admin/pages/media/fields/Collections.tsx +21 -11
  27. package/src/admin/pages/media/fields/Content.tsx +150 -0
  28. package/src/admin/pages/media/fields/Image.tsx +43 -10
  29. package/src/admin/pages/media/media-item-store.ts +16 -2
  30. package/src/admin/types/media-item.ts +9 -0
  31. package/src/components/SearchInput.astro +3 -3
  32. package/src/components/VideoPlayer.astro +74 -10
  33. package/src/i18n/react/prepare-i18n-config.ts +1 -1
  34. package/src/i18n/react/use-i18n.ts +1 -1
  35. package/src/i18n/translations/TRANSLATION-STATUS.md +4 -0
  36. package/src/i18n/translations/en.yml +3 -4
  37. package/src/i18n/translations/ur.yml +25 -0
  38. package/src/i18n/translations.ts +1 -0
  39. package/src/layouts/Page.astro +0 -2
  40. package/src/layouts/components/Menu.astro +1 -1
  41. package/src/pages/404Route.astro +3 -1
  42. package/src/pages/details-page/components/AudioPanel.astro +1 -12
  43. package/src/pages/details-page/components/AudioPlayer.astro +40 -8
  44. package/src/pages/details-page/utils/create-content-metadata.ts +2 -1
  45. package/src/pages/search-page/components/LoadingSkeleton.tsx +20 -13
  46. package/src/pages/search-page/components/SearchFilter.tsx +2 -2
  47. package/src/pages/search-page/components/SearchList.tsx +33 -29
  48. package/src/pages/search-page/components/Select.tsx +15 -13
  49. package/src/layouts/components/PreloadReact.tsx +0 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # lightnet
2
2
 
3
+ ## 3.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#351](https://github.com/LightNetDev/LightNet/pull/351) [`a1f63bf`](https://github.com/LightNetDev/LightNet/commit/a1f63bfa30448fdf58bfb1ff24007caa750349e0) Thanks [@smn-cds](https://github.com/smn-cds)! - Add an external fallback UI for audio content so non-mp3 URLs show a player-style link that opens in a new tab.
8
+
9
+ - [#351](https://github.com/LightNetDev/LightNet/pull/351) [`a1f63bf`](https://github.com/LightNetDev/LightNet/commit/a1f63bfa30448fdf58bfb1ff24007caa750349e0) Thanks [@smn-cds](https://github.com/smn-cds)! - Add a fallback in the video player so any video URL can be used on the "video" details page. Unsupported providers now render a poster-style link that opens the video on the external site.
10
+
11
+ ## 3.11.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#347](https://github.com/LightNetDev/LightNet/pull/347) [`17ea3d9`](https://github.com/LightNetDev/LightNet/commit/17ea3d9b599d9456b36e1974d3a8f5dc7d39fb65) Thanks [@ajjn](https://github.com/ajjn)! - Add translation for Urdu
16
+
17
+ ### Patch Changes
18
+
19
+ - [#344](https://github.com/LightNetDev/LightNet/pull/344) [`a48d2a7`](https://github.com/LightNetDev/LightNet/commit/a48d2a7ec9b782c763950b02cdbe420b1ed000ec) Thanks [@smn-cds](https://github.com/smn-cds)! - Remove preload React logic
20
+
3
21
  ## 3.10.8
4
22
 
5
23
  ### Patch Changes
@@ -139,7 +139,7 @@ test.describe("Media item edit page", () => {
139
139
  }
140
140
 
141
141
  const getPublishButton = (page: Page) =>
142
- page.getByRole("button", { name: "Publish Changes" }).first()
142
+ page.getByRole("button", { name: "Publish changes" }).first()
143
143
 
144
144
  const expectPublishedMessage = (page: Page) =>
145
145
  expect(
@@ -347,4 +347,38 @@ test.describe("Media item edit page", () => {
347
347
  "true",
348
348
  )
349
349
  })
350
+
351
+ test("should not submit the form on enter", async ({ page, lightnet }) => {
352
+ await lightnet("/admin/media/faithful-freestyle--en")
353
+ const writeFileRequest = await recordWriteFile(page)
354
+ const writePromise = writeFileRequest()
355
+
356
+ const updatedTitle = "Faithful Freestyle (Enter key)"
357
+ const titleInput = page.getByLabel("Title")
358
+ await titleInput.fill(updatedTitle)
359
+
360
+ const publishButton = getPublishButton(page)
361
+ await expect(publishButton).toBeEnabled()
362
+
363
+ await titleInput.press("Enter")
364
+
365
+ const enterResult = await Promise.race([
366
+ writePromise.then(() => "submitted"),
367
+ page.waitForTimeout(500).then(() => "timeout"),
368
+ ])
369
+ expect(enterResult).toBe("timeout")
370
+ await expect(publishButton).toBeEnabled()
371
+
372
+ await publishButton.click()
373
+
374
+ const expectedMediaItem = JSON.parse(
375
+ await readFile(faithfulFreestyleMediaUrl, "utf-8"),
376
+ )
377
+ const { body } = await writePromise
378
+ expect(body).toEqual({
379
+ ...expectedMediaItem,
380
+ title: updatedTitle,
381
+ })
382
+ await expectPublishedMessage(page)
383
+ })
350
384
  })
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.1_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.3_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.1_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.3_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.55.1_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.55.1_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.1_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.3_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.1_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.3_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.55.1_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.55.1_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../astro/astro.js" "$@"
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../tailwindcss/lib/cli.js" "$@"
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../tailwindcss/lib/cli.js" "$@"
@@ -7,12 +7,12 @@
7
7
  "@astrojs/react": "^4.4.2",
8
8
  "@astrojs/tailwind": "^6.0.2",
9
9
  "@lightnet/decap-admin": "^3.1.4",
10
- "astro": "^5.16.1",
11
- "lightnet": "^3.10.7",
12
- "react": "^19.2.0",
13
- "react-dom": "^19.2.0",
10
+ "astro": "^5.16.6",
11
+ "lightnet": "^3.12.0",
12
+ "react": "^19.2.3",
13
+ "react-dom": "^19.2.3",
14
14
  "sharp": "^0.34.5",
15
- "tailwindcss": "^3.4.18",
15
+ "tailwindcss": "^3.4.19",
16
16
  "typescript": "^5.9.3"
17
17
  },
18
18
  "engines": {
@@ -102,9 +102,29 @@ test("Should create complete content metadata", () => {
102
102
  type: "package",
103
103
  },
104
104
  },
105
- ].forEach(({ url, expected }) => {
106
- test(`Should create content metadata for url ${url}`, () => {
107
- expect(createContentMetadata({ url })).toMatchObject(expected)
105
+ {
106
+ url: "/some.zip",
107
+ label: "foo",
108
+ expected: {
109
+ label: "foo",
110
+ isExternal: false,
111
+ extension: "zip",
112
+ type: "package",
113
+ },
114
+ },
115
+ {
116
+ url: "/some.zip",
117
+ label: "",
118
+ expected: {
119
+ label: "some",
120
+ isExternal: false,
121
+ extension: "zip",
122
+ type: "package",
123
+ },
124
+ },
125
+ ].forEach(({ url, expected, label }) => {
126
+ test(`Should create content metadata for url '${url}' ${label !== undefined ? `and label '${label}'` : ""}`, () => {
127
+ expect(createContentMetadata({ url, label })).toMatchObject(expected)
108
128
  })
109
129
  })
110
130
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "LightNet makes it easy to run your own digital media library.",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "3.10.8",
6
+ "version": "3.12.0",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LightNetDev/lightnet",
@@ -50,24 +50,24 @@
50
50
  "@hookform/resolvers": "^5.2.2",
51
51
  "@iconify-json/mdi": "^1.2.3",
52
52
  "@iconify/tailwind": "^1.2.0",
53
- "@mdxeditor/editor": "^3.50.0",
53
+ "@mdxeditor/editor": "^3.52.3",
54
54
  "@tailwindcss/typography": "^0.5.19",
55
- "@tanstack/react-virtual": "^3.13.12",
55
+ "@tanstack/react-virtual": "^3.13.17",
56
56
  "daisyui": "^4.12.24",
57
57
  "embla-carousel": "^8.6.0",
58
58
  "embla-carousel-wheel-gestures": "^8.1.0",
59
59
  "fuse.js": "^7.1.0",
60
- "i18next": "^25.6.3",
60
+ "i18next": "^25.7.3",
61
61
  "marked": "^16.4.2",
62
- "react-hook-form": "^7.66.1",
63
- "yaml": "^2.8.1"
62
+ "react-hook-form": "^7.70.0",
63
+ "yaml": "^2.8.2"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@playwright/test": "^1.57.0",
67
- "@types/node": "^22.19.1",
67
+ "@types/node": "^22.19.3",
68
68
  "@types/react": "^19.2.7",
69
69
  "typescript": "^5.9.3",
70
- "vitest": "^4.0.14"
70
+ "vitest": "^4.0.16"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=22"
@@ -12,66 +12,118 @@ import { useI18n } from "../../../i18n/react/use-i18n"
12
12
  import ErrorMessage from "./atoms/ErrorMessage"
13
13
  import Hint from "./atoms/Hint"
14
14
  import Label from "./atoms/Label"
15
+ import { useFieldDirty } from "./hooks/use-field-dirty"
15
16
  import { useFieldError } from "./hooks/use-field-error"
17
+ import { getBorderClass } from "./utils/get-border-class"
16
18
 
17
19
  export default function DynamicArray<TFieldValues extends FieldValues>({
18
20
  control,
19
21
  name,
20
22
  label,
23
+ required = false,
21
24
  hint,
22
25
  renderElement,
23
- addButton,
26
+ renderElementMeta,
27
+ renderAddButton,
24
28
  }: {
25
29
  name: ArrayPath<TFieldValues>
30
+ required?: boolean
26
31
  label: string
27
32
  hint?: string
28
33
  control: Control<TFieldValues>
29
34
  renderElement: (index: number) => ReactNode
30
- addButton: {
31
- label: string
32
- onClick: (
33
- append: UseFieldArrayAppend<TFieldValues, ArrayPath<TFieldValues>>,
34
- elementIndex: number,
35
- ) => void
36
- }
35
+ renderElementMeta?: (index: number) => ReactNode
36
+ renderAddButton: (args: {
37
+ addElement: UseFieldArrayAppend<TFieldValues, ArrayPath<TFieldValues>>
38
+ index: number
39
+ }) => ReactNode
37
40
  }) {
38
- const { fields, append, remove } = useFieldArray({
41
+ const { fields, append, remove, swap } = useFieldArray({
39
42
  name,
40
43
  control,
41
44
  })
42
- const { t } = useI18n()
43
45
  const errorMessage = useFieldError({ control, name })
46
+ const isDirty = useFieldDirty({ control, name })
44
47
  return (
45
48
  <fieldset key={name}>
46
49
  <legend>
47
- <Label label={label} />
50
+ <Label
51
+ required={required}
52
+ label={label}
53
+ isDirty={isDirty}
54
+ isInvalid={!!errorMessage}
55
+ />
48
56
  </legend>
49
57
 
50
- <div className="flex w-full flex-col divide-y divide-gray-300 rounded-lg rounded-ss-none border border-gray-300 bg-gray-100 shadow-sm">
58
+ <div
59
+ className={`flex w-full flex-col gap-1 rounded-xl rounded-ss-none ${getBorderClass({ isDirty, errorMessage })} bg-slate-200 p-1 shadow-inner`}
60
+ >
51
61
  {fields.map((field, index) => (
52
- <div className="flex w-full items-center gap-2 p-2" key={field.id}>
53
- <div className="flex grow flex-col">{renderElement(index)}</div>
54
- <button
55
- className="flex items-center rounded-md p-2 text-gray-600 transition-colors ease-in-out hover:text-rose-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-700"
56
- type="button"
57
- onClick={() => remove(index)}
62
+ <div
63
+ className="w-full gap-2 rounded-xl bg-slate-50 px-2 pb-4 shadow-sm"
64
+ key={field.id}
65
+ >
66
+ <div
67
+ className={`flex items-center ${renderElementMeta ? "justify-between" : "justify-end"}`}
58
68
  >
59
- <Icon className="mdi--remove" ariaLabel={t("ln.admin.remove")} />
60
- </button>
69
+ {renderElementMeta && renderElementMeta(index)}
70
+ <div className="-me-2 flex">
71
+ <ItemActionButton
72
+ icon="mdi--arrow-up"
73
+ label="ln.admin.move-up"
74
+ disabled={index === 0}
75
+ onClick={() => swap(index, index - 1)}
76
+ />
77
+ <ItemActionButton
78
+ icon="mdi--arrow-down"
79
+ label="ln.admin.move-down"
80
+ disabled={index === fields.length - 1}
81
+ onClick={() => swap(index, index + 1)}
82
+ />
83
+ <ItemActionButton
84
+ icon="mdi--remove"
85
+ label="ln.admin.remove"
86
+ onClick={() => remove(index)}
87
+ className="hover:!text-rose-800"
88
+ />
89
+ </div>
90
+ </div>
91
+
92
+ {renderElement(index)}
61
93
  </div>
62
94
  ))}
63
- <button
64
- type="button"
65
- className="rounded-b-lg p-4 text-sm font-bold text-gray-500 transition-colors ease-in-out hover:bg-sky-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-700"
66
- onClick={() => {
67
- addButton.onClick(append, fields.length)
68
- }}
69
- >
70
- {t(addButton.label)}
71
- </button>
95
+ <div className="flex flex-col items-center py-2">
96
+ {renderAddButton({ addElement: append, index: fields.length })}
97
+ </div>
72
98
  </div>
73
99
  <ErrorMessage message={errorMessage} />
74
100
  <Hint preserveSpace={true} label={hint} />
75
101
  </fieldset>
76
102
  )
77
103
  }
104
+
105
+ function ItemActionButton({
106
+ label,
107
+ icon,
108
+ onClick,
109
+ disabled = false,
110
+ className,
111
+ }: {
112
+ label: string
113
+ icon: string
114
+ disabled?: boolean
115
+ onClick: () => void
116
+ className?: string
117
+ }) {
118
+ const { t } = useI18n()
119
+ return (
120
+ <button
121
+ className={`flex items-center rounded-xl p-2 text-slate-600 transition-colors ease-in-out hover:text-sky-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-700 disabled:text-slate-300 ${className}`}
122
+ type="button"
123
+ disabled={disabled}
124
+ onClick={onClick}
125
+ >
126
+ <Icon className={icon} ariaLabel={t(label)} />
127
+ </button>
128
+ )
129
+ }
@@ -1,19 +1,27 @@
1
1
  import type { InputHTMLAttributes } from "react"
2
- import { type Control, type FieldValues, type Path } from "react-hook-form"
2
+ import {
3
+ type Control,
4
+ type FieldValues,
5
+ type Path,
6
+ type RegisterOptions,
7
+ } from "react-hook-form"
3
8
 
4
9
  import ErrorMessage from "./atoms/ErrorMessage"
5
10
  import Hint from "./atoms/Hint"
6
11
  import Label from "./atoms/Label"
7
12
  import { useFieldDirty } from "./hooks/use-field-dirty"
8
13
  import { useFieldError } from "./hooks/use-field-error"
14
+ import { getBorderClass } from "./utils/get-border-class"
9
15
 
10
16
  type Props<TFieldValues extends FieldValues> = {
11
17
  name: Path<TFieldValues>
18
+ required?: boolean
12
19
  label?: string
13
20
  labelSize?: "small" | "medium"
14
21
  hint?: string
15
22
  preserveHintSpace?: boolean
16
23
  control: Control<TFieldValues>
24
+ registerOptions?: RegisterOptions<TFieldValues>
17
25
  } & InputHTMLAttributes<HTMLInputElement>
18
26
 
19
27
  export default function Input<TFieldValues extends FieldValues>({
@@ -23,10 +31,14 @@ export default function Input<TFieldValues extends FieldValues>({
23
31
  hint,
24
32
  preserveHintSpace = true,
25
33
  control,
34
+ className,
35
+ required = false,
36
+ registerOptions,
26
37
  ...inputProps
27
38
  }: Props<TFieldValues>) {
28
39
  const isDirty = useFieldDirty({ control, name })
29
40
  const errorMessage = useFieldError({ control, name })
41
+
30
42
  return (
31
43
  <div key={name} className="group flex w-full flex-col">
32
44
  {label && (
@@ -36,15 +48,17 @@ export default function Input<TFieldValues extends FieldValues>({
36
48
  size={labelSize}
37
49
  isDirty={isDirty}
38
50
  isInvalid={!!errorMessage}
51
+ required={required}
39
52
  />
40
53
  </label>
41
54
  )}
42
55
 
43
56
  <input
44
- className={`dy-input dy-input-bordered border-gray-300 shadow-inner focus:border-sky-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-sky-700 ${isDirty && !errorMessage ? "border-gray-700" : ""} ${errorMessage ? "border-rose-800" : ""} ${label ? "rounded-ss-none" : ""}`}
57
+ className={`rounded-xl ${getBorderClass({ isDirty, errorMessage })} px-4 py-3 shadow-inner disabled:text-slate-500 ${label ? "rounded-ss-none" : ""} ${className}`}
45
58
  id={name}
46
59
  aria-invalid={!!errorMessage}
47
- {...control.register(name)}
60
+ aria-required={required}
61
+ {...control.register(name, registerOptions)}
48
62
  {...inputProps}
49
63
  />
50
64
  <ErrorMessage message={errorMessage} />
@@ -6,6 +6,8 @@ import {
6
6
  type CodeBlockEditorProps,
7
7
  codeBlockPlugin,
8
8
  CreateLink,
9
+ diffSourcePlugin,
10
+ DiffSourceToggleWrapper,
9
11
  headingsPlugin,
10
12
  linkDialogPlugin,
11
13
  linkPlugin,
@@ -22,6 +24,7 @@ import {
22
24
  type Control,
23
25
  Controller,
24
26
  type FieldValues,
27
+ get,
25
28
  type Path,
26
29
  } from "react-hook-form"
27
30
 
@@ -42,12 +45,15 @@ export default function LazyLoadedMarkdownEditor<
42
45
  <Controller
43
46
  control={control}
44
47
  name={name}
45
- render={({ field: { onBlur, onChange, value, ref } }) => (
48
+ render={({
49
+ field: { onBlur, onChange, value, ref },
50
+ formState: { defaultValues },
51
+ }) => (
46
52
  <MDXEditor
47
53
  markdown={value ?? ""}
48
54
  onBlur={onBlur}
49
55
  onChange={onChange}
50
- contentEditableClassName="prose bg-gray-50 h-80 w-full max-w-full overflow-y-auto"
56
+ contentEditableClassName="prose bg-slate-50 h-80 w-full max-w-full overflow-y-auto"
51
57
  ref={ref}
52
58
  onError={(error) =>
53
59
  console.error("Error while editing markdown", error)
@@ -68,17 +74,18 @@ export default function LazyLoadedMarkdownEditor<
68
74
  linkDialogPlugin(),
69
75
  quotePlugin(),
70
76
  markdownShortcutPlugin(),
77
+ diffSourcePlugin({ diffMarkdown: get(defaultValues, name) }),
71
78
  toolbarPlugin({
72
79
  toolbarContents: () => (
73
- <>
80
+ <DiffSourceToggleWrapper>
74
81
  <UndoRedo />
75
82
  <BoldItalicUnderlineToggles />
76
83
  <BlockTypeSelect />
77
84
  <ListsToggle options={["bullet", "number"]} />
78
85
  <CreateLink />
79
- </>
86
+ </DiffSourceToggleWrapper>
80
87
  ),
81
- toolbarClassName: "!rounded-none",
88
+ toolbarClassName: "!rounded-none !bg-slate-200",
82
89
  }),
83
90
  ]}
84
91
  />
@@ -6,6 +6,7 @@ import Hint from "./atoms/Hint"
6
6
  import Label from "./atoms/Label"
7
7
  import { useFieldDirty } from "./hooks/use-field-dirty"
8
8
  import { useFieldError } from "./hooks/use-field-error"
9
+ import { getBorderClass } from "./utils/get-border-class"
9
10
 
10
11
  const LazyLoadedMarkdownEditor = lazy(
11
12
  () => import("./LazyLoadedMarkdownEditor"),
@@ -14,11 +15,13 @@ const LazyLoadedMarkdownEditor = lazy(
14
15
  export default function MarkdownEditor<TFieldValues extends FieldValues>({
15
16
  control,
16
17
  name,
18
+ required = false,
17
19
  label,
18
20
  hint,
19
21
  }: {
20
22
  name: Path<TFieldValues>
21
23
  label: string
24
+ required?: boolean
22
25
  hint?: string
23
26
  control: Control<TFieldValues>
24
27
  }) {
@@ -28,16 +31,21 @@ export default function MarkdownEditor<TFieldValues extends FieldValues>({
28
31
  return (
29
32
  <fieldset key={name} className="group">
30
33
  <legend>
31
- <Label label={label} isDirty={isDirty} isInvalid={!!errorMessage} />
34
+ <Label
35
+ required={required}
36
+ label={label}
37
+ isDirty={isDirty}
38
+ isInvalid={!!errorMessage}
39
+ />
32
40
  </legend>
33
41
 
34
42
  <div
35
- className={`overflow-hidden rounded-lg rounded-ss-none border border-gray-300 shadow-sm group-focus-within:border-sky-700 group-focus-within:ring-1 group-focus-within:ring-sky-700 ${isDirty && !errorMessage ? "border-gray-700" : ""} ${errorMessage ? "border-rose-800" : ""}`}
43
+ className={`overflow-hidden rounded-xl rounded-ss-none ${getBorderClass({ isDirty, errorMessage, focusWithin: true })} shadow-sm`}
36
44
  >
37
45
  <Suspense
38
46
  fallback={
39
- <div className="h-[22.75rem] w-full bg-gray-50">
40
- <div className="h-10 bg-gray-100"></div>
47
+ <div className="h-[22.75rem] w-full bg-slate-50">
48
+ <div className="h-10 bg-slate-100"></div>
41
49
  </div>
42
50
  }
43
51
  >
@@ -1,15 +1,18 @@
1
1
  import { type Control, type FieldValues, type Path } from "react-hook-form"
2
2
 
3
+ import Icon from "../../../components/Icon"
3
4
  import ErrorMessage from "./atoms/ErrorMessage"
4
5
  import Hint from "./atoms/Hint"
5
6
  import Label from "./atoms/Label"
6
7
  import { useFieldDirty } from "./hooks/use-field-dirty"
7
8
  import { useFieldError } from "./hooks/use-field-error"
9
+ import { getBorderClass } from "./utils/get-border-class"
8
10
 
9
11
  export default function Select<TFieldValues extends FieldValues>({
10
12
  name,
11
13
  label,
12
14
  labelSize,
15
+ required = false,
13
16
  control,
14
17
  defaultValue,
15
18
  hint,
@@ -20,6 +23,7 @@ export default function Select<TFieldValues extends FieldValues>({
20
23
  label?: string
21
24
  labelSize?: "small" | "medium"
22
25
  hint?: string
26
+ required?: boolean
23
27
  preserveHintSpace?: boolean
24
28
  defaultValue?: string
25
29
  control: Control<TFieldValues>
@@ -35,23 +39,31 @@ export default function Select<TFieldValues extends FieldValues>({
35
39
  label={label}
36
40
  size={labelSize}
37
41
  isDirty={isDirty}
42
+ required={required}
38
43
  isInvalid={!!errorMessage}
39
44
  />
40
45
  </label>
41
46
  )}
42
- <select
43
- {...control.register(name)}
44
- id={name}
45
- aria-invalid={!!errorMessage}
46
- defaultValue={defaultValue}
47
- className={`dy-select dy-select-bordered text-base shadow-sm focus:border-sky-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-sky-700 ${isDirty && !errorMessage ? "border-gray-700" : ""} ${errorMessage ? "border-rose-800" : ""} ${label ? "rounded-ss-none" : ""}`}
48
- >
49
- {options.map(({ id, labelText }) => (
50
- <option key={id} value={id}>
51
- {labelText ?? id}
52
- </option>
53
- ))}
54
- </select>
47
+ <div className="relative">
48
+ <select
49
+ {...control.register(name)}
50
+ id={name}
51
+ aria-invalid={!!errorMessage}
52
+ aria-required={required}
53
+ defaultValue={defaultValue}
54
+ className={`w-full appearance-none rounded-xl ${getBorderClass({ isDirty, errorMessage })} bg-white px-4 py-3 pe-12 shadow-sm ${label ? "rounded-ss-none" : ""}`}
55
+ >
56
+ {options.map(({ id, labelText }) => (
57
+ <option key={id} value={id}>
58
+ {labelText ?? id}
59
+ </option>
60
+ ))}
61
+ </select>
62
+ <Icon
63
+ className="absolute end-3 top-1/2 -translate-y-1/2 text-lg text-slate-600 mdi--chevron-down"
64
+ ariaLabel=""
65
+ />
66
+ </div>
55
67
  <ErrorMessage message={errorMessage} />
56
68
  <Hint preserveSpace={preserveHintSpace} label={hint} />
57
69
  </div>
@@ -8,10 +8,10 @@ import type { MediaItem } from "../../types/media-item"
8
8
  const SUCCESS_DURATION_MS = 2000
9
9
 
10
10
  const baseButtonClass =
11
- "flex min-w-52 items-center justify-center gap-2 rounded-2xl px-4 py-3 font-bold shadow-sm transition-colors easy-in-out focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-sky-700 focus-visible:ring-offset-1 disabled:cursor-not-allowed"
11
+ "flex min-w-52 items-center justify-center gap-2 rounded-xl px-4 py-3 font-bold shadow-sm transition-colors easy-in-out focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-sky-700 focus-visible:ring-offset-1 disabled:cursor-not-allowed"
12
12
 
13
13
  const buttonStateClasses = {
14
- idle: "bg-gray-800 text-gray-50 hover:bg-gray-950 hover:text-gray-300 disabled:bg-gray-500 disabled:text-gray-300",
14
+ idle: "bg-slate-800 text-slate-50 hover:bg-slate-950 hover:text-slate-300 disabled:bg-slate-500 disabled:text-slate-300",
15
15
  error:
16
16
  "bg-rose-700 text-white hover:bg-rose-800 hover:text-white disabled:bg-rose-600",
17
17
  success:
@@ -0,0 +1,27 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from "react"
2
+
3
+ type Props = {
4
+ variant: "secondary"
5
+ children?: ReactNode
6
+ } & ButtonHTMLAttributes<HTMLButtonElement>
7
+
8
+ export default function Button({
9
+ children,
10
+ className,
11
+ variant,
12
+ ...buttonProps
13
+ }: Props) {
14
+ const styles = {
15
+ secondary: "text-slate-800 bg-slate-50 hover:bg-sky-50",
16
+ } as const
17
+
18
+ return (
19
+ <button
20
+ className={`flex items-center gap-1 rounded-xl px-10 py-4 text-sm font-bold shadow-sm transition-colors ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-700 ${styles[variant]} ${className}`}
21
+ type="button"
22
+ {...buttonProps}
23
+ >
24
+ {children}
25
+ </button>
26
+ )
27
+ }