lightnet 3.10.6 → 3.10.8

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 (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/__e2e__/admin.spec.ts +19 -19
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/package.json +2 -2
  5. package/__tests__/utils/markdown.spec.ts +16 -0
  6. package/package.json +7 -7
  7. package/src/admin/components/form/DynamicArray.tsx +10 -7
  8. package/src/admin/components/form/Input.tsx +31 -16
  9. package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +34 -10
  10. package/src/admin/components/form/MarkdownEditor.tsx +10 -8
  11. package/src/admin/components/form/Select.tsx +21 -6
  12. package/src/admin/components/form/SubmitButton.tsx +12 -7
  13. package/src/admin/components/form/atoms/ErrorMessage.tsx +1 -1
  14. package/src/admin/components/form/atoms/FileUpload.tsx +115 -0
  15. package/src/admin/components/form/atoms/Hint.tsx +12 -3
  16. package/src/admin/components/form/atoms/Label.tsx +15 -12
  17. package/src/admin/components/form/hooks/use-field-dirty.tsx +12 -0
  18. package/src/admin/components/form/hooks/use-field-error.tsx +23 -2
  19. package/src/admin/i18n/admin-i18n.ts +21 -0
  20. package/src/admin/i18n/translations/en.yml +15 -9
  21. package/src/admin/pages/AdminRoute.astro +0 -2
  22. package/src/admin/pages/media/EditForm.tsx +16 -13
  23. package/src/admin/pages/media/EditRoute.astro +31 -15
  24. package/src/admin/pages/media/fields/Authors.tsx +4 -28
  25. package/src/admin/pages/media/fields/Categories.tsx +5 -38
  26. package/src/admin/pages/media/fields/Collections.tsx +19 -78
  27. package/src/admin/pages/media/fields/Image.tsx +86 -0
  28. package/src/admin/pages/media/file-system.ts +6 -2
  29. package/src/admin/pages/media/media-item-store.ts +32 -3
  30. package/src/admin/types/media-item.ts +20 -0
  31. package/src/astro-integration/config.ts +10 -0
  32. package/src/astro-integration/integration.ts +7 -3
  33. package/src/components/HighlightSection.astro +1 -1
  34. package/src/content/get-media-items.ts +2 -1
  35. package/src/i18n/react/i18n-context.ts +16 -5
  36. package/src/layouts/Page.astro +5 -4
  37. package/src/layouts/components/LanguagePicker.astro +11 -5
  38. package/src/layouts/components/Menu.astro +76 -10
  39. package/src/pages/details-page/components/main-details/EditButton.astro +1 -1
  40. package/src/pages/details-page/components/main-details/OpenButton.astro +1 -1
  41. package/src/pages/search-page/components/LoadingSkeleton.tsx +1 -1
  42. package/src/pages/search-page/components/SearchListItem.tsx +1 -1
  43. package/src/utils/markdown.ts +6 -0
  44. package/src/admin/components/form/atoms/Legend.tsx +0 -20
  45. /package/src/i18n/react/{useI18n.ts → use-i18n.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # lightnet
2
2
 
3
+ ## 3.10.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [#340](https://github.com/LightNetDev/LightNet/pull/340) [`6162179`](https://github.com/LightNetDev/LightNet/commit/6162179b90e987359934bd08ac9d4500eb560e25) Thanks [@smn-cds](https://github.com/smn-cds)! - Tighten locale detection to avoid partial matches
8
+
9
+ When the URL path starts with a locale-like prefix, only full segment matches should be treated as a locale. Previously `/enx` was parsed as locale `en`; now only `/en/...` (or just `/en`) will match.
10
+
11
+ - [#346](https://github.com/LightNetDev/LightNet/pull/346) [`2bc1560`](https://github.com/LightNetDev/LightNet/commit/2bc15600b0a9bcfc3410f155191b7ea1c9c5306a) Thanks [@smn-cds](https://github.com/smn-cds)! - Fixes an issue where top menus would not close when tapping outside the menu on iOS Safari.
12
+ This update also improves overall menu accessibility.
13
+
14
+ - [#345](https://github.com/LightNetDev/LightNet/pull/345) [`1e6cddb`](https://github.com/LightNetDev/LightNet/commit/1e6cddba0e783f9d59da7d36dd154dbcd784ed6b) Thanks [@smn-cds](https://github.com/smn-cds)! - Fix menu not working on ios safari
15
+
16
+ - [#340](https://github.com/LightNetDev/LightNet/pull/340) [`6162179`](https://github.com/LightNetDev/LightNet/commit/6162179b90e987359934bd08ac9d4500eb560e25) Thanks [@smn-cds](https://github.com/smn-cds)! - Update dependencies
17
+
18
+ ## 3.10.7
19
+
20
+ ### Patch Changes
21
+
22
+ - [#337](https://github.com/LightNetDev/LightNet/pull/337) [`ac32a7f`](https://github.com/LightNetDev/LightNet/commit/ac32a7f35b58d5aff39dd45531d77247ae24c61f) Thanks [@smn-cds](https://github.com/smn-cds)! - Do not use ALL CAPS casing for primary buttons.
23
+
24
+ - [#337](https://github.com/LightNetDev/LightNet/pull/337) [`ac32a7f`](https://github.com/LightNetDev/LightNet/commit/ac32a7f35b58d5aff39dd45531d77247ae24c61f) Thanks [@smn-cds](https://github.com/smn-cds)! - Update dependencies
25
+
3
26
  ## 3.10.6
4
27
 
5
28
  ### Patch Changes
@@ -25,13 +25,13 @@ test.describe("Edit button on details page", () => {
25
25
  await expect(editButton).toBeHidden()
26
26
  })
27
27
 
28
- test("Should show `Edit` button on book details page after visiting `/en/admin/` path.", async ({
28
+ test("Should show `Edit` button on book details page after visiting `/admin/` path.", async ({
29
29
  page,
30
30
  lightnet,
31
31
  }) => {
32
32
  const ln = await lightnet()
33
33
 
34
- await page.goto(ln.resolveURL("/en/admin/"))
34
+ await page.goto(ln.resolveURL("/admin/"))
35
35
  await expect(
36
36
  page.getByText("Admin features are enabled now.", { exact: true }),
37
37
  ).toBeVisible()
@@ -45,15 +45,15 @@ test.describe("Edit button on details page", () => {
45
45
  await expect(editButton).toBeVisible()
46
46
  await expect(editButton).toHaveAttribute(
47
47
  "href",
48
- "/en/admin/media/faithful-freestyle--en",
48
+ "/admin/media/faithful-freestyle--en",
49
49
  )
50
50
  })
51
51
 
52
- test("Should show `Edit` button on video details page after visiting `/en/admin/` path.", async ({
52
+ test("Should show `Edit` button on video details page after visiting `/admin/` path.", async ({
53
53
  page,
54
54
  lightnet,
55
55
  }) => {
56
- const ln = await lightnet("/en/admin/")
56
+ const ln = await lightnet("/admin/")
57
57
 
58
58
  await expect(
59
59
  page.getByText("Admin features are enabled now.", { exact: true }),
@@ -68,15 +68,15 @@ test.describe("Edit button on details page", () => {
68
68
  await expect(editButton).toBeVisible()
69
69
  await expect(editButton).toHaveAttribute(
70
70
  "href",
71
- "/en/admin/media/how-to-kickflip--de",
71
+ "/admin/media/how-to-kickflip--de",
72
72
  )
73
73
  })
74
74
 
75
- test("Should show `Edit` button on audio details page after visiting `/en/admin/` path.", async ({
75
+ test("Should show `Edit` button on audio details page after visiting `/admin/` path.", async ({
76
76
  page,
77
77
  lightnet,
78
78
  }) => {
79
- const ln = await lightnet("/en/admin/")
79
+ const ln = await lightnet("/admin/")
80
80
 
81
81
  await expect(
82
82
  page.getByText("Admin features are enabled now.", { exact: true }),
@@ -91,7 +91,7 @@ test.describe("Edit button on details page", () => {
91
91
  await expect(editButton).toBeVisible()
92
92
  await expect(editButton).toHaveAttribute(
93
93
  "href",
94
- "/en/admin/media/skate-sounds--en",
94
+ "/admin/media/skate-sounds--en",
95
95
  )
96
96
  })
97
97
 
@@ -99,7 +99,7 @@ test.describe("Edit button on details page", () => {
99
99
  page,
100
100
  lightnet,
101
101
  }) => {
102
- const ln = await lightnet("/en/admin/")
102
+ const ln = await lightnet("/admin/")
103
103
  await ln.goto("/en/media/faithful-freestyle--en")
104
104
 
105
105
  const editButton = page.locator("#edit-btn")
@@ -107,7 +107,7 @@ test.describe("Edit button on details page", () => {
107
107
 
108
108
  await editButton.click()
109
109
  await expect(page).toHaveURL(
110
- ln.resolveURL("/en/admin/media/faithful-freestyle--en"),
110
+ ln.resolveURL("/admin/media/faithful-freestyle--en"),
111
111
  )
112
112
  await expect(
113
113
  page.getByText("Edit media item", { exact: false }),
@@ -147,7 +147,7 @@ test.describe("Media item edit page", () => {
147
147
  ).toBeVisible()
148
148
 
149
149
  test("should edit title", async ({ page, lightnet }) => {
150
- await lightnet("/en/admin/media/faithful-freestyle--en")
150
+ await lightnet("/admin/media/faithful-freestyle--en")
151
151
  const writeFileRequest = await recordWriteFile(page)
152
152
 
153
153
  const updatedTitle = "Faithful Freestyle (Edited)"
@@ -175,7 +175,7 @@ test.describe("Media item edit page", () => {
175
175
  })
176
176
 
177
177
  test("Should update media type", async ({ page, lightnet }) => {
178
- await lightnet("/en/admin/media/faithful-freestyle--en")
178
+ await lightnet("/admin/media/faithful-freestyle--en")
179
179
  const writeFileRequest = await recordWriteFile(page)
180
180
 
181
181
  const typeSelect = page.getByLabel("Type").first()
@@ -198,7 +198,7 @@ test.describe("Media item edit page", () => {
198
198
  })
199
199
 
200
200
  test("Should update author name", async ({ page, lightnet }) => {
201
- await lightnet("/en/admin/media/faithful-freestyle--en")
201
+ await lightnet("/admin/media/faithful-freestyle--en")
202
202
  const writeFileRequest = await recordWriteFile(page)
203
203
 
204
204
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
@@ -223,7 +223,7 @@ test.describe("Media item edit page", () => {
223
223
  })
224
224
 
225
225
  test("Should add author", async ({ page, lightnet }) => {
226
- await lightnet("/en/admin/media/faithful-freestyle--en")
226
+ await lightnet("/admin/media/faithful-freestyle--en")
227
227
  const writeFileRequest = await recordWriteFile(page)
228
228
 
229
229
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
@@ -249,7 +249,7 @@ test.describe("Media item edit page", () => {
249
249
  })
250
250
 
251
251
  test("Should remove author", async ({ page, lightnet }) => {
252
- await lightnet("/en/admin/media/faithful-freestyle--en")
252
+ await lightnet("/admin/media/faithful-freestyle--en")
253
253
  const writeFileRequest = await recordWriteFile(page)
254
254
 
255
255
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
@@ -283,7 +283,7 @@ test.describe("Media item edit page", () => {
283
283
  page,
284
284
  lightnet,
285
285
  }) => {
286
- await lightnet("/en/admin/media/faithful-freestyle--en")
286
+ await lightnet("/admin/media/faithful-freestyle--en")
287
287
 
288
288
  const commonIdInput = page.getByLabel("Common ID")
289
289
  await expect(commonIdInput).toHaveValue("faithful-freestyle")
@@ -302,7 +302,7 @@ test.describe("Media item edit page", () => {
302
302
  page,
303
303
  lightnet,
304
304
  }) => {
305
- await lightnet("/en/admin/media/faithful-freestyle--en")
305
+ await lightnet("/admin/media/faithful-freestyle--en")
306
306
 
307
307
  const categoriesFieldset = page.getByRole("group", { name: "Categories" })
308
308
  await page.getByRole("button", { name: "Add Category" }).click()
@@ -325,7 +325,7 @@ test.describe("Media item edit page", () => {
325
325
  page,
326
326
  lightnet,
327
327
  }) => {
328
- await lightnet("/en/admin/media/faithful-freestyle--en")
328
+ await lightnet("/admin/media/faithful-freestyle--en")
329
329
 
330
330
  const categoriesFieldset = page.getByRole("group", { name: "Categories" })
331
331
  await page.getByRole("button", { name: "Add Category" }).click()
@@ -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.15.8_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.15.8_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.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"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.15.8_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.15.8_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.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"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../astro/astro.js" "$@"
@@ -7,8 +7,8 @@
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.15.8",
11
- "lightnet": "^3.10.5",
10
+ "astro": "^5.16.1",
11
+ "lightnet": "^3.10.7",
12
12
  "react": "^19.2.0",
13
13
  "react-dom": "^19.2.0",
14
14
  "sharp": "^0.34.5",
@@ -27,12 +27,28 @@ test("Should remove headers", () => {
27
27
  expect(markdownToText("# H1\n## H2 words#")).toBe("H1\nH2 words#")
28
28
  })
29
29
 
30
+ test("Should remove underline", () => {
31
+ expect(markdownToText("Some <u>underlined</u> Words /u >")).toBe(
32
+ "Some underlined Words /u >",
33
+ )
34
+ })
35
+
36
+ test("Should remove encoded space", () => {
37
+ expect(markdownToText("Text&#x20;with&#x20;space&#x20;")).toBe(
38
+ "Text with space ",
39
+ )
40
+ })
41
+
30
42
  test("Should remove inline modifiers", async () => {
31
43
  expect(markdownToText("this is **some bold** and _italic_ text")).toBe(
32
44
  "this is some bold and italic text",
33
45
  )
34
46
  })
35
47
 
48
+ test("Should remove code blocks", async () => {
49
+ expect(markdownToText("some\n```js \ncode\n```\n")).toBe("some\ncode\n")
50
+ })
51
+
36
52
  test("Should remove list", () => {
37
53
  expect(markdownToText("- this is **bold**\n- this is normal")).toBe(
38
54
  "this is bold\nthis is normal",
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.6",
6
+ "version": "3.10.8",
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.49.1",
53
+ "@mdxeditor/editor": "^3.50.0",
54
54
  "@tailwindcss/typography": "^0.5.19",
55
55
  "@tanstack/react-virtual": "^3.13.12",
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.2",
60
+ "i18next": "^25.6.3",
61
61
  "marked": "^16.4.2",
62
- "react-hook-form": "^7.66.0",
62
+ "react-hook-form": "^7.66.1",
63
63
  "yaml": "^2.8.1"
64
64
  },
65
65
  "devDependencies": {
66
- "@playwright/test": "^1.56.1",
66
+ "@playwright/test": "^1.57.0",
67
67
  "@types/node": "^22.19.1",
68
- "@types/react": "^19.2.5",
68
+ "@types/react": "^19.2.7",
69
69
  "typescript": "^5.9.3",
70
- "vitest": "^4.0.9"
70
+ "vitest": "^4.0.14"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=22"
@@ -8,10 +8,10 @@ import {
8
8
  } from "react-hook-form"
9
9
 
10
10
  import Icon from "../../../components/Icon"
11
- import { useI18n } from "../../../i18n/react/useI18n"
11
+ import { useI18n } from "../../../i18n/react/use-i18n"
12
12
  import ErrorMessage from "./atoms/ErrorMessage"
13
13
  import Hint from "./atoms/Hint"
14
- import Legend from "./atoms/Legend"
14
+ import Label from "./atoms/Label"
15
15
  import { useFieldError } from "./hooks/use-field-error"
16
16
 
17
17
  export default function DynamicArray<TFieldValues extends FieldValues>({
@@ -43,13 +43,16 @@ export default function DynamicArray<TFieldValues extends FieldValues>({
43
43
  const errorMessage = useFieldError({ control, name })
44
44
  return (
45
45
  <fieldset key={name}>
46
- <Legend label={label} />
47
- <div className="flex w-full flex-col divide-y divide-gray-300 overflow-hidden rounded-lg border border-gray-300 bg-gray-100 shadow-sm">
46
+ <legend>
47
+ <Label label={label} />
48
+ </legend>
49
+
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">
48
51
  {fields.map((field, index) => (
49
52
  <div className="flex w-full items-center gap-2 p-2" key={field.id}>
50
53
  <div className="flex grow flex-col">{renderElement(index)}</div>
51
54
  <button
52
- className="flex items-center p-2 text-gray-600 transition-colors ease-in-out hover:text-rose-800"
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"
53
56
  type="button"
54
57
  onClick={() => remove(index)}
55
58
  >
@@ -59,7 +62,7 @@ export default function DynamicArray<TFieldValues extends FieldValues>({
59
62
  ))}
60
63
  <button
61
64
  type="button"
62
- className="p-4 text-sm font-bold text-gray-500 transition-colors ease-in-out hover:bg-gray-200"
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"
63
66
  onClick={() => {
64
67
  addButton.onClick(append, fields.length)
65
68
  }}
@@ -68,7 +71,7 @@ export default function DynamicArray<TFieldValues extends FieldValues>({
68
71
  </button>
69
72
  </div>
70
73
  <ErrorMessage message={errorMessage} />
71
- <Hint label={hint} />
74
+ <Hint preserveSpace={true} label={hint} />
72
75
  </fieldset>
73
76
  )
74
77
  }
@@ -1,39 +1,54 @@
1
+ import type { InputHTMLAttributes } from "react"
1
2
  import { type Control, type FieldValues, type Path } from "react-hook-form"
2
3
 
3
4
  import ErrorMessage from "./atoms/ErrorMessage"
4
5
  import Hint from "./atoms/Hint"
5
6
  import Label from "./atoms/Label"
7
+ import { useFieldDirty } from "./hooks/use-field-dirty"
6
8
  import { useFieldError } from "./hooks/use-field-error"
7
9
 
10
+ type Props<TFieldValues extends FieldValues> = {
11
+ name: Path<TFieldValues>
12
+ label?: string
13
+ labelSize?: "small" | "medium"
14
+ hint?: string
15
+ preserveHintSpace?: boolean
16
+ control: Control<TFieldValues>
17
+ } & InputHTMLAttributes<HTMLInputElement>
18
+
8
19
  export default function Input<TFieldValues extends FieldValues>({
9
20
  name,
10
21
  label,
11
- defaultValue,
22
+ labelSize,
12
23
  hint,
24
+ preserveHintSpace = true,
13
25
  control,
14
- type = "text",
15
- }: {
16
- name: Path<TFieldValues>
17
- label: string
18
- defaultValue?: string
19
- hint?: string
20
- control: Control<TFieldValues>
21
- type?: "text" | "date"
22
- }) {
26
+ ...inputProps
27
+ }: Props<TFieldValues>) {
28
+ const isDirty = useFieldDirty({ control, name })
23
29
  const errorMessage = useFieldError({ control, name })
24
30
  return (
25
- <div key={name} className="flex w-full flex-col">
26
- <Label for={name} label={label} />
31
+ <div key={name} className="group flex w-full flex-col">
32
+ {label && (
33
+ <label htmlFor={name}>
34
+ <Label
35
+ label={label}
36
+ size={labelSize}
37
+ isDirty={isDirty}
38
+ isInvalid={!!errorMessage}
39
+ />
40
+ </label>
41
+ )}
42
+
27
43
  <input
28
- className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
29
- type={type}
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" : ""}`}
30
45
  id={name}
31
- defaultValue={defaultValue}
32
46
  aria-invalid={!!errorMessage}
33
47
  {...control.register(name)}
48
+ {...inputProps}
34
49
  />
35
50
  <ErrorMessage message={errorMessage} />
36
- <Hint label={hint} />
51
+ <Hint preserveSpace={preserveHintSpace} label={hint} />
37
52
  </div>
38
53
  )
39
54
  }
@@ -3,18 +3,20 @@ import "@mdxeditor/editor/style.css"
3
3
  import {
4
4
  BlockTypeSelect,
5
5
  BoldItalicUnderlineToggles,
6
+ type CodeBlockEditorProps,
7
+ codeBlockPlugin,
6
8
  CreateLink,
7
- diffSourcePlugin,
8
- DiffSourceToggleWrapper,
9
9
  headingsPlugin,
10
10
  linkDialogPlugin,
11
11
  linkPlugin,
12
12
  listsPlugin,
13
13
  ListsToggle,
14
+ markdownShortcutPlugin,
14
15
  MDXEditor,
15
16
  quotePlugin,
16
17
  toolbarPlugin,
17
18
  UndoRedo,
19
+ useCodeBlockEditorContext,
18
20
  } from "@mdxeditor/editor"
19
21
  import {
20
22
  type Control,
@@ -31,12 +33,10 @@ export default function LazyLoadedMarkdownEditor<
31
33
  TFieldValues extends FieldValues,
32
34
  >({
33
35
  control,
34
- defaultValue,
35
36
  name,
36
37
  }: {
37
38
  name: Path<TFieldValues>
38
39
  control: Control<TFieldValues>
39
- defaultValue?: string
40
40
  }) {
41
41
  return (
42
42
  <Controller
@@ -49,26 +49,36 @@ export default function LazyLoadedMarkdownEditor<
49
49
  onChange={onChange}
50
50
  contentEditableClassName="prose bg-gray-50 h-80 w-full max-w-full overflow-y-auto"
51
51
  ref={ref}
52
+ onError={(error) =>
53
+ console.error("Error while editing markdown", error)
54
+ }
52
55
  plugins={[
53
56
  headingsPlugin(),
54
57
  listsPlugin(),
55
58
  linkPlugin(),
56
- linkDialogPlugin(),
57
- diffSourcePlugin({
58
- viewMode: "rich-text",
59
- diffMarkdown: defaultValue,
59
+ codeBlockPlugin({
60
+ codeBlockEditorDescriptors: [
61
+ {
62
+ match: () => true,
63
+ priority: 0,
64
+ Editor: TextAreaCodeEditor,
65
+ },
66
+ ],
60
67
  }),
68
+ linkDialogPlugin(),
61
69
  quotePlugin(),
70
+ markdownShortcutPlugin(),
62
71
  toolbarPlugin({
63
72
  toolbarContents: () => (
64
- <DiffSourceToggleWrapper>
73
+ <>
65
74
  <UndoRedo />
66
75
  <BoldItalicUnderlineToggles />
67
76
  <BlockTypeSelect />
68
77
  <ListsToggle options={["bullet", "number"]} />
69
78
  <CreateLink />
70
- </DiffSourceToggleWrapper>
79
+ </>
71
80
  ),
81
+ toolbarClassName: "!rounded-none",
72
82
  }),
73
83
  ]}
74
84
  />
@@ -76,3 +86,17 @@ export default function LazyLoadedMarkdownEditor<
76
86
  />
77
87
  )
78
88
  }
89
+
90
+ function TextAreaCodeEditor(props: CodeBlockEditorProps) {
91
+ const cb = useCodeBlockEditorContext()
92
+ return (
93
+ <div onKeyDown={(e) => e.nativeEvent.stopImmediatePropagation()}>
94
+ <textarea
95
+ rows={3}
96
+ cols={20}
97
+ defaultValue={props.code}
98
+ onChange={(e) => cb.setCode(e.target.value)}
99
+ />
100
+ </div>
101
+ )
102
+ }
@@ -3,7 +3,8 @@ import { type Control, type FieldValues, type Path } from "react-hook-form"
3
3
 
4
4
  import ErrorMessage from "./atoms/ErrorMessage"
5
5
  import Hint from "./atoms/Hint"
6
- import Legend from "./atoms/Legend"
6
+ import Label from "./atoms/Label"
7
+ import { useFieldDirty } from "./hooks/use-field-dirty"
7
8
  import { useFieldError } from "./hooks/use-field-error"
8
9
 
9
10
  const LazyLoadedMarkdownEditor = lazy(
@@ -12,7 +13,6 @@ const LazyLoadedMarkdownEditor = lazy(
12
13
 
13
14
  export default function MarkdownEditor<TFieldValues extends FieldValues>({
14
15
  control,
15
- defaultValue,
16
16
  name,
17
17
  label,
18
18
  hint,
@@ -21,15 +21,18 @@ export default function MarkdownEditor<TFieldValues extends FieldValues>({
21
21
  label: string
22
22
  hint?: string
23
23
  control: Control<TFieldValues>
24
- defaultValue?: string
25
24
  }) {
25
+ const isDirty = useFieldDirty({ control, name })
26
26
  const errorMessage = useFieldError({ control, name })
27
27
 
28
28
  return (
29
- <fieldset key={name}>
30
- <Legend label={label} />
29
+ <fieldset key={name} className="group">
30
+ <legend>
31
+ <Label label={label} isDirty={isDirty} isInvalid={!!errorMessage} />
32
+ </legend>
33
+
31
34
  <div
32
- className={`overflow-hidden rounded-lg border border-gray-300 shadow-sm ${errorMessage ? "border-rose-800" : ""}`}
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" : ""}`}
33
36
  >
34
37
  <Suspense
35
38
  fallback={
@@ -41,12 +44,11 @@ export default function MarkdownEditor<TFieldValues extends FieldValues>({
41
44
  <LazyLoadedMarkdownEditor
42
45
  control={control as Control<any>}
43
46
  name={name}
44
- defaultValue={defaultValue}
45
47
  />
46
48
  </Suspense>
47
49
  </div>
48
50
  <ErrorMessage message={errorMessage} />
49
- <Hint label={hint} />
51
+ <Hint preserveSpace={true} label={hint} />
50
52
  </fieldset>
51
53
  )
52
54
  }
@@ -1,35 +1,50 @@
1
- import type { Control, FieldValues, Path } from "react-hook-form"
1
+ import { type Control, type FieldValues, type Path } from "react-hook-form"
2
2
 
3
3
  import ErrorMessage from "./atoms/ErrorMessage"
4
4
  import Hint from "./atoms/Hint"
5
5
  import Label from "./atoms/Label"
6
+ import { useFieldDirty } from "./hooks/use-field-dirty"
6
7
  import { useFieldError } from "./hooks/use-field-error"
7
8
 
8
9
  export default function Select<TFieldValues extends FieldValues>({
9
10
  name,
10
11
  label,
12
+ labelSize,
11
13
  control,
12
14
  defaultValue,
13
15
  hint,
16
+ preserveHintSpace = true,
14
17
  options,
15
18
  }: {
16
19
  name: Path<TFieldValues>
17
- label: string
20
+ label?: string
21
+ labelSize?: "small" | "medium"
18
22
  hint?: string
23
+ preserveHintSpace?: boolean
19
24
  defaultValue?: string
20
25
  control: Control<TFieldValues>
21
26
  options: { id: string; labelText?: string }[]
22
27
  }) {
28
+ const isDirty = useFieldDirty({ control, name })
23
29
  const errorMessage = useFieldError({ control, name })
24
30
  return (
25
- <div key={name} className="flex w-full flex-col">
26
- <Label for={name} label={label} />
31
+ <div key={name} className="group flex w-full flex-col">
32
+ {label && (
33
+ <label htmlFor={name}>
34
+ <Label
35
+ label={label}
36
+ size={labelSize}
37
+ isDirty={isDirty}
38
+ isInvalid={!!errorMessage}
39
+ />
40
+ </label>
41
+ )}
27
42
  <select
28
43
  {...control.register(name)}
29
44
  id={name}
30
45
  aria-invalid={!!errorMessage}
31
46
  defaultValue={defaultValue}
32
- className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
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" : ""}`}
33
48
  >
34
49
  {options.map(({ id, labelText }) => (
35
50
  <option key={id} value={id}>
@@ -38,7 +53,7 @@ export default function Select<TFieldValues extends FieldValues>({
38
53
  ))}
39
54
  </select>
40
55
  <ErrorMessage message={errorMessage} />
41
- <Hint label={hint} />
56
+ <Hint preserveSpace={preserveHintSpace} label={hint} />
42
57
  </div>
43
58
  )
44
59
  }
@@ -2,16 +2,16 @@ import { useEffect, useRef, useState } from "react"
2
2
  import { type Control, useFormState } from "react-hook-form"
3
3
 
4
4
  import Icon from "../../../components/Icon"
5
- import { useI18n } from "../../../i18n/react/useI18n"
5
+ import { useI18n } from "../../../i18n/react/use-i18n"
6
6
  import type { MediaItem } from "../../types/media-item"
7
7
 
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 uppercase shadow-sm transition-colors easy-in-out focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900 disabled:cursor-not-allowed"
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"
12
12
 
13
13
  const buttonStateClasses = {
14
- idle: "bg-gray-800 text-gray-100 hover:bg-gray-950 hover:text-gray-300 disabled:bg-gray-600 disabled:text-gray-200",
14
+ idle: "bg-gray-800 text-gray-50 hover:bg-gray-950 hover:text-gray-300 disabled:bg-gray-500 disabled:text-gray-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:
@@ -40,9 +40,10 @@ export default function SubmitButton({
40
40
  className?: string
41
41
  }) {
42
42
  const { t } = useI18n()
43
- const { isSubmitting, isSubmitSuccessful, submitCount } = useFormState({
44
- control,
45
- })
43
+ const { isSubmitting, isSubmitSuccessful, submitCount, isDirty } =
44
+ useFormState({
45
+ control,
46
+ })
46
47
 
47
48
  const buttonState = useButtonState(isSubmitSuccessful, submitCount)
48
49
  const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]} ${className}`
@@ -50,7 +51,11 @@ export default function SubmitButton({
50
51
  const icon = icons[buttonState]
51
52
 
52
53
  return (
53
- <button className={buttonClass} type="submit" disabled={isSubmitting}>
54
+ <button
55
+ className={buttonClass}
56
+ type="submit"
57
+ disabled={isSubmitting || !isDirty}
58
+ >
54
59
  {icon && <Icon className={icon} ariaLabel="" />}
55
60
  {t(label)}
56
61
  </button>