lightnet 3.10.6 → 3.10.7

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # lightnet
2
2
 
3
+ ## 3.10.7
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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.
8
+
9
+ - [#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
10
+
3
11
  ## 3.10.6
4
12
 
5
13
  ### Patch Changes
@@ -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.15.9_@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.15.9_@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.15.9_@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.15.9_@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.15.9",
11
+ "lightnet": "^3.10.6",
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.7",
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.49.3",
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
66
  "@playwright/test": "^1.56.1",
67
67
  "@types/node": "^22.19.1",
68
- "@types/react": "^19.2.5",
68
+ "@types/react": "^19.2.6",
69
69
  "typescript": "^5.9.3",
70
- "vitest": "^4.0.9"
70
+ "vitest": "^4.0.10"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=22"
@@ -11,7 +11,7 @@ import Icon from "../../../components/Icon"
11
11
  import { useI18n } from "../../../i18n/react/useI18n"
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-blue-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-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-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-blue-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-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-blue-700 group-focus-within:ring-1 group-focus-within:ring-blue-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-blue-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-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
  }
@@ -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 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-blue-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>
@@ -1,9 +1,18 @@
1
1
  import { useI18n } from "../../../../i18n/react/useI18n"
2
2
 
3
- export default function Hint({ label }: { label?: string }) {
3
+ export default function Hint({
4
+ label,
5
+ preserveSpace,
6
+ }: {
7
+ label?: string
8
+ preserveSpace: boolean
9
+ }) {
4
10
  const { t } = useI18n()
11
+ if (!preserveSpace && !label) {
12
+ return null
13
+ }
5
14
  return (
6
- <div className="flex h-12 w-full items-start justify-end p-2">
15
+ <div className="flex h-8 w-full items-start justify-end p-2">
7
16
  {label && <span className="dy-label-text-alt">{t(label)}</span>}
8
17
  </div>
9
18
  )
@@ -2,22 +2,25 @@ import { useI18n } from "../../../../i18n/react/useI18n"
2
2
 
3
3
  export default function Label({
4
4
  label,
5
- for: htmlFor,
6
- size = "sm",
7
- className,
5
+ size = "medium",
6
+ className = "",
7
+ isDirty,
8
+ isInvalid,
8
9
  }: {
9
10
  label: string
10
- for: string
11
11
  className?: string
12
- size?: "sm" | "xs"
12
+ size?: "small" | "medium"
13
+ isDirty?: boolean
14
+ isInvalid?: boolean
13
15
  }) {
14
16
  const { t } = useI18n()
15
17
  return (
16
- <label
17
- htmlFor={htmlFor}
18
- className={`font-bold uppercase text-gray-600 ${size === "sm" ? "pb-2 text-sm" : "pb-1 text-xs"} ${className}`}
19
- >
20
- {t(label)}
21
- </label>
18
+ <div className="flex">
19
+ <span
20
+ className={`rounded-t-md bg-gray-300 px-4 font-bold text-gray-700 shadow-sm transition-colors duration-150 group-focus-within:bg-blue-700 group-focus-within:text-gray-50 group-focus-within:ring-1 group-focus-within:ring-blue-700 ${size === "medium" ? "py-2 text-sm" : "py-1 text-xs"} ${isDirty ? "bg-gray-700 !text-white" : ""} ${isInvalid ? "bg-rose-800 !text-gray-50" : ""} ${className}`}
21
+ >
22
+ {t(label)}
23
+ </span>
24
+ </div>
22
25
  )
23
26
  }
@@ -0,0 +1,12 @@
1
+ import { type Control, get, useFormState } from "react-hook-form"
2
+
3
+ export function useFieldDirty({
4
+ control,
5
+ name,
6
+ }: {
7
+ control: Control<any>
8
+ name: string
9
+ }) {
10
+ const { dirtyFields } = useFormState({ control, name, exact: true })
11
+ return get(dirtyFields, name) as boolean | undefined
12
+ }
@@ -1,24 +1,24 @@
1
1
  ln.admin.edit: Edit
2
- ln.admin.publish-changes: Publish Changes
2
+ ln.admin.publish-changes: Publish changes
3
3
  ln.admin.published: Published
4
- ln.admin.save-changes: Save Changes
4
+ ln.admin.save-changes: Save changes
5
5
  ln.admin.saved: Saved
6
6
  ln.admin.failed: Failed
7
7
  ln.admin.remove: Remove
8
8
  ln.admin.name: Name
9
- ln.admin.add-author: Add Author
10
- ln.admin.add-category: Add Category
9
+ ln.admin.add-author: Add author
10
+ ln.admin.add-category: Add category
11
11
  ln.admin.collections: Collections
12
- ln.admin.add-collection: Add Collection
13
- ln.admin.edit-media-item: Edit media item
12
+ ln.admin.add-collection: Add collection
13
+ ln.admin.edit-media-item: Edit Media Item
14
14
  ln.admin.position-in-collection: Position in Collection
15
15
  ln.admin.back-to-details-page: Back to details page
16
16
  ln.admin.title: Title
17
17
  ln.admin.common-id: Common ID
18
18
  ln.admin.authors: Authors
19
19
  ln.admin.description: Description
20
- ln.admin.created-on: Created on
21
- ln.admin.created-on-hint: When has this item been created on this library?
20
+ ln.admin.date-created: Date Created
21
+ ln.admin.date-created-hint: When has this item been created on this library?
22
22
  ln.admin.common-id-hint: The English title, all lowercase, words separated with hyphens.
23
23
  ln.admin.errors.non-empty-string: Please enter at least one character.
24
24
  ln.admin.errors.invalid-date: That date doesn't look right.
@@ -36,8 +36,6 @@ export default function EditForm({
36
36
  collections: SelectOption[]
37
37
  }) {
38
38
  const { handleSubmit, control } = useForm({
39
- // Provide per-input defaults so SSG prerender matches, but keep a full
40
- // defaultValues object here because useFieldArray does not accept default values.
41
39
  defaultValues: mediaItem,
42
40
  mode: "onTouched",
43
41
  shouldFocusError: true,
@@ -85,8 +83,8 @@ export default function EditForm({
85
83
  <Authors control={control} defaultValue={mediaItem.authors} />
86
84
  <Input
87
85
  name="dateCreated"
88
- label="ln.admin.created-on"
89
- hint="ln.admin.created-on-hint"
86
+ label="ln.admin.date-created"
87
+ hint="ln.admin.date-created-hint"
90
88
  type="date"
91
89
  defaultValue={mediaItem.dateCreated}
92
90
  control={control}
@@ -105,7 +103,6 @@ export default function EditForm({
105
103
  control={control}
106
104
  name="description"
107
105
  label="ln.admin.description"
108
- defaultValue={mediaItem.description}
109
106
  />
110
107
 
111
108
  <SubmitButton className="self-end" control={control} />
@@ -58,7 +58,7 @@ const languages = config.languages.map(({ code, label }) => ({
58
58
  }))
59
59
  ---
60
60
 
61
- <Page mainClass="bg-slate-500">
61
+ <Page mainClass="bg-gray-700">
62
62
  <div class="mx-auto block max-w-screen-md px-4 md:px-8">
63
63
  <a
64
64
  class="block pb-4 pt-8 text-gray-200 underline"
@@ -1,8 +1,7 @@
1
1
  import { type Control } from "react-hook-form"
2
2
 
3
- import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
4
3
  import DynamicArray from "../../../components/form/DynamicArray"
5
- import { useFieldError } from "../../../components/form/hooks/use-field-error"
4
+ import Input from "../../../components/form/Input"
6
5
  import type { MediaItem } from "../../../types/media-item"
7
6
 
8
7
  export default function Authors({
@@ -18,8 +17,9 @@ export default function Authors({
18
17
  name="authors"
19
18
  label="ln.admin.authors"
20
19
  renderElement={(index) => (
21
- <AuthorInput
22
- index={index}
20
+ <Input
21
+ name={`authors.${index}.value`}
22
+ preserveHintSpace={false}
23
23
  control={control}
24
24
  defaultValue={defaultValue[index]?.value}
25
25
  />
@@ -32,27 +32,3 @@ export default function Authors({
32
32
  />
33
33
  )
34
34
  }
35
-
36
- function AuthorInput({
37
- index,
38
- control,
39
- defaultValue,
40
- }: {
41
- index: number
42
- control: Control<MediaItem>
43
- defaultValue?: string
44
- }) {
45
- const name = `authors.${index}.value` as const
46
- const errorMessage = useFieldError({ name, control })
47
- return (
48
- <>
49
- <input
50
- className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
51
- aria-invalid={!!errorMessage}
52
- defaultValue={defaultValue}
53
- {...control.register(name)}
54
- />
55
- <ErrorMessage message={errorMessage} />
56
- </>
57
- )
58
- }
@@ -1,8 +1,7 @@
1
1
  import { type Control } from "react-hook-form"
2
2
 
3
- import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
4
3
  import DynamicArray from "../../../components/form/DynamicArray"
5
- import { useFieldError } from "../../../components/form/hooks/use-field-error"
4
+ import Select from "../../../components/form/Select"
6
5
  import type { MediaItem } from "../../../types/media-item"
7
6
 
8
7
  export default function Categories({
@@ -20,11 +19,12 @@ export default function Categories({
20
19
  name="categories"
21
20
  label="ln.categories"
22
21
  renderElement={(index) => (
23
- <CategorySelect
24
- categories={categories}
22
+ <Select
23
+ options={categories}
25
24
  control={control}
26
- index={index}
25
+ name={`categories.${index}.value`}
27
26
  defaultValue={defaultValue[index]?.value}
27
+ preserveHintSpace={false}
28
28
  />
29
29
  )}
30
30
  addButton={{
@@ -35,36 +35,3 @@ export default function Categories({
35
35
  />
36
36
  )
37
37
  }
38
-
39
- function CategorySelect({
40
- control,
41
- categories,
42
- defaultValue,
43
- index,
44
- }: {
45
- control: Control<MediaItem>
46
- categories: { id: string; labelText: string }[]
47
- defaultValue?: string
48
- index: number
49
- }) {
50
- const name = `categories.${index}.value` as const
51
- const errorMessage = useFieldError({ name, control })
52
- return (
53
- <>
54
- <select
55
- {...control.register(name)}
56
- id={name}
57
- defaultValue={defaultValue}
58
- aria-invalid={!!errorMessage}
59
- className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
60
- >
61
- {categories.map(({ id, labelText }) => (
62
- <option key={id} value={id}>
63
- {labelText}
64
- </option>
65
- ))}
66
- </select>
67
- <ErrorMessage message={errorMessage} />
68
- </>
69
- )
70
- }
@@ -1,9 +1,8 @@
1
1
  import { type Control } from "react-hook-form"
2
2
 
3
- import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
4
- import Label from "../../../components/form/atoms/Label"
5
3
  import DynamicArray from "../../../components/form/DynamicArray"
6
- import { useFieldError } from "../../../components/form/hooks/use-field-error"
4
+ import Input from "../../../components/form/Input"
5
+ import Select from "../../../components/form/Select"
7
6
  import type { MediaItem } from "../../../types/media-item"
8
7
 
9
8
  export default function Collections({
@@ -21,17 +20,28 @@ export default function Collections({
21
20
  name="collections"
22
21
  label="ln.admin.collections"
23
22
  renderElement={(index) => (
24
- <div className="flex w-full flex-col py-2">
25
- <CollectionSelect
26
- collections={collections}
23
+ <div className="flex w-full flex-col gap-4 py-2">
24
+ <Select
25
+ options={collections}
26
+ label="ln.admin.name"
27
+ labelSize="small"
28
+ preserveHintSpace={false}
29
+ name={`collections.${index}.collection`}
27
30
  control={control}
28
- index={index}
29
31
  defaultValue={defaultValue[index]?.collection}
30
32
  />
31
- <CollectionIndex
33
+ <Input
34
+ type="number"
32
35
  control={control}
33
- index={index}
36
+ label="ln.admin.position-in-collection"
37
+ labelSize="small"
38
+ step={1}
39
+ min={0}
40
+ preserveHintSpace={false}
34
41
  defaultValue={defaultValue[index]?.index}
42
+ {...control.register(`collections.${index}.index`, {
43
+ setValueAs: (value) => (value === "" ? undefined : Number(value)),
44
+ })}
35
45
  />
36
46
  </div>
37
47
  )}
@@ -46,72 +56,3 @@ export default function Collections({
46
56
  />
47
57
  )
48
58
  }
49
-
50
- function CollectionSelect({
51
- control,
52
- collections,
53
- index,
54
- defaultValue,
55
- }: {
56
- control: Control<MediaItem>
57
- collections: { id: string; labelText: string }[]
58
- defaultValue?: string
59
- index: number
60
- }) {
61
- const name = `collections.${index}.collection` as const
62
- const errorMessage = useFieldError({ name, control })
63
- return (
64
- <>
65
- <Label for={name} label="ln.admin.name" size="xs" />
66
- <select
67
- {...control.register(name)}
68
- id={name}
69
- defaultValue={defaultValue}
70
- aria-invalid={!!errorMessage}
71
- className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
72
- >
73
- {collections.map(({ id, labelText }) => (
74
- <option key={id} value={id}>
75
- {labelText}
76
- </option>
77
- ))}
78
- </select>
79
- <ErrorMessage message={errorMessage} />
80
- </>
81
- )
82
- }
83
-
84
- function CollectionIndex({
85
- control,
86
- index,
87
- defaultValue,
88
- }: {
89
- control: Control<MediaItem>
90
- index: number
91
- defaultValue?: number
92
- }) {
93
- const name = `collections.${index}.index` as const
94
- const errorMessage = useFieldError({ name, control })
95
- return (
96
- <>
97
- <Label
98
- for={name}
99
- label="ln.admin.position-in-collection"
100
- size="xs"
101
- className="mt-3"
102
- />
103
- <input
104
- className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
105
- aria-invalid={!!errorMessage}
106
- type="number"
107
- id={name}
108
- defaultValue={defaultValue}
109
- step={1}
110
- {...control.register(name, {
111
- setValueAs: (value) => (value === "" ? undefined : Number(value)),
112
- })}
113
- />
114
- <ErrorMessage message={errorMessage} />
115
- </>
116
- )
117
- }
@@ -53,7 +53,7 @@ const { image, id, title, text, link, className, titleClass, textClass } =
53
53
  {
54
54
  link && (
55
55
  <a
56
- class="inline-flex items-center justify-center gap-2 rounded-2xl bg-primary px-6 py-3 text-sm font-bold uppercase text-gray-50 shadow-sm hover:bg-primary/85 hover:text-gray-100"
56
+ class="inline-flex items-center justify-center gap-2 rounded-2xl bg-primary px-6 py-3 text-sm font-bold text-gray-50 shadow-sm hover:bg-primary/85 hover:text-gray-100"
57
57
  href={link.href}
58
58
  >
59
59
  {link.text}
@@ -14,7 +14,7 @@ const content = createContentMetadata(item.data.content[0])
14
14
  ---
15
15
 
16
16
  <a
17
- class="flex min-w-52 items-center justify-center gap-2 rounded-2xl bg-gray-800 px-6 py-3 font-bold uppercase text-gray-100 shadow-sm hover:bg-gray-950 hover:text-gray-300"
17
+ class="flex min-w-52 items-center justify-center gap-2 rounded-2xl bg-gray-800 px-6 py-3 font-bold text-gray-100 shadow-sm hover:bg-gray-950 hover:text-gray-300"
18
18
  href={content.url}
19
19
  target={content.target}
20
20
  hreflang={item.data.language}
@@ -19,6 +19,12 @@ export function markdownToText(markdown?: string) {
19
19
  .replaceAll(/^#+ ?/gm, "")
20
20
  // lists
21
21
  .replaceAll(/^- /gm, "")
22
+ // escaped white space
23
+ .replaceAll(/&#x20;/g, " ")
24
+ // underlines
25
+ .replaceAll(/<\/?u>/g, "")
26
+ // code block
27
+ .replaceAll(/^```[a-zA-Z ]*\n/gm, "")
22
28
  // block quotes
23
29
  .replaceAll(/^>+ ?/gm, "")
24
30
  // bold and italics
@@ -1,20 +0,0 @@
1
- import { useI18n } from "../../../../i18n/react/useI18n"
2
-
3
- export default function Legend({
4
- label,
5
- size = "sm",
6
- className,
7
- }: {
8
- label: string
9
- className?: string
10
- size?: "sm" | "xs"
11
- }) {
12
- const { t } = useI18n()
13
- return (
14
- <legend
15
- className={`pb-2 font-bold uppercase text-gray-600 ${size === "sm" ? "text-sm" : "text-xs"} ${className}`}
16
- >
17
- {t(label)}
18
- </legend>
19
- )
20
- }