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.
- package/CHANGELOG.md +18 -0
- package/__e2e__/admin.spec.ts +35 -1
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
- package/__e2e__/fixtures/basics/package.json +5 -5
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +23 -3
- package/package.json +8 -8
- package/src/admin/components/form/DynamicArray.tsx +81 -29
- package/src/admin/components/form/Input.tsx +17 -3
- package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +12 -5
- package/src/admin/components/form/MarkdownEditor.tsx +12 -4
- package/src/admin/components/form/Select.tsx +25 -13
- package/src/admin/components/form/SubmitButton.tsx +2 -2
- package/src/admin/components/form/atoms/Button.tsx +27 -0
- package/src/admin/components/form/atoms/FileUpload.tsx +124 -49
- package/src/admin/components/form/atoms/Hint.tsx +2 -2
- package/src/admin/components/form/atoms/Label.tsx +17 -6
- package/src/admin/components/form/utils/get-border-class.ts +22 -0
- package/src/admin/i18n/translations/en.yml +19 -3
- package/src/admin/pages/AdminRoute.astro +1 -1
- package/src/admin/pages/media/EditForm.tsx +33 -15
- package/src/admin/pages/media/EditRoute.astro +2 -2
- package/src/admin/pages/media/fields/Authors.tsx +15 -5
- package/src/admin/pages/media/fields/Categories.tsx +17 -5
- package/src/admin/pages/media/fields/Collections.tsx +21 -11
- package/src/admin/pages/media/fields/Content.tsx +150 -0
- package/src/admin/pages/media/fields/Image.tsx +43 -10
- package/src/admin/pages/media/media-item-store.ts +16 -2
- package/src/admin/types/media-item.ts +9 -0
- package/src/components/SearchInput.astro +3 -3
- package/src/components/VideoPlayer.astro +74 -10
- package/src/i18n/react/prepare-i18n-config.ts +1 -1
- package/src/i18n/react/use-i18n.ts +1 -1
- package/src/i18n/translations/TRANSLATION-STATUS.md +4 -0
- package/src/i18n/translations/en.yml +3 -4
- package/src/i18n/translations/ur.yml +25 -0
- package/src/i18n/translations.ts +1 -0
- package/src/layouts/Page.astro +0 -2
- package/src/layouts/components/Menu.astro +1 -1
- package/src/pages/404Route.astro +3 -1
- package/src/pages/details-page/components/AudioPanel.astro +1 -12
- package/src/pages/details-page/components/AudioPlayer.astro +40 -8
- package/src/pages/details-page/utils/create-content-metadata.ts +2 -1
- package/src/pages/search-page/components/LoadingSkeleton.tsx +20 -13
- package/src/pages/search-page/components/SearchFilter.tsx +2 -2
- package/src/pages/search-page/components/SearchList.tsx +33 -29
- package/src/pages/search-page/components/Select.tsx +15 -13
- 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
|
package/__e2e__/admin.spec.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
11
|
-
"lightnet": "^3.
|
|
12
|
-
"react": "^19.2.
|
|
13
|
-
"react-dom": "^19.2.
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
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.
|
|
53
|
+
"@mdxeditor/editor": "^3.52.3",
|
|
54
54
|
"@tailwindcss/typography": "^0.5.19",
|
|
55
|
-
"@tanstack/react-virtual": "^3.13.
|
|
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.
|
|
60
|
+
"i18next": "^25.7.3",
|
|
61
61
|
"marked": "^16.4.2",
|
|
62
|
-
"react-hook-form": "^7.
|
|
63
|
-
"yaml": "^2.8.
|
|
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.
|
|
67
|
+
"@types/node": "^22.19.3",
|
|
68
68
|
"@types/react": "^19.2.7",
|
|
69
69
|
"typescript": "^5.9.3",
|
|
70
|
-
"vitest": "^4.0.
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
50
|
+
<Label
|
|
51
|
+
required={required}
|
|
52
|
+
label={label}
|
|
53
|
+
isDirty={isDirty}
|
|
54
|
+
isInvalid={!!errorMessage}
|
|
55
|
+
/>
|
|
48
56
|
</legend>
|
|
49
57
|
|
|
50
|
-
<div
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
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 {
|
|
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={`
|
|
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
|
-
{
|
|
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={({
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
40
|
-
<div className="h-10 bg-
|
|
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
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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-
|
|
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-
|
|
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
|
+
}
|