lightnet 3.12.0 → 3.12.2

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 (47) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/__e2e__/admin.spec.ts +1 -365
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/package.json +3 -3
  5. package/package.json +8 -15
  6. package/src/astro-integration/config.ts +1 -22
  7. package/src/astro-integration/integration.ts +4 -39
  8. package/src/content/content-schema.ts +0 -7
  9. package/src/content/get-media-items.ts +1 -47
  10. package/src/i18n/translate.ts +1 -1
  11. package/src/i18n/translations/TRANSLATION-STATUS.md +37 -12
  12. package/src/i18n/translations/en.yml +5 -0
  13. package/src/i18n/translations.ts +0 -7
  14. package/src/layouts/Page.astro +2 -0
  15. package/src/layouts/global.css +3 -0
  16. package/src/pages/details-page/components/main-details/EditButton.astro +2 -4
  17. package/src/pages/search-page/components/SearchFilter.tsx +1 -1
  18. package/src/admin/api/fs/write-file.ts +0 -53
  19. package/src/admin/components/form/DynamicArray.tsx +0 -129
  20. package/src/admin/components/form/Input.tsx +0 -68
  21. package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +0 -109
  22. package/src/admin/components/form/MarkdownEditor.tsx +0 -62
  23. package/src/admin/components/form/Select.tsx +0 -71
  24. package/src/admin/components/form/SubmitButton.tsx +0 -86
  25. package/src/admin/components/form/atoms/Button.tsx +0 -27
  26. package/src/admin/components/form/atoms/ErrorMessage.tsx +0 -13
  27. package/src/admin/components/form/atoms/FileUpload.tsx +0 -190
  28. package/src/admin/components/form/atoms/Hint.tsx +0 -19
  29. package/src/admin/components/form/atoms/Label.tsx +0 -37
  30. package/src/admin/components/form/hooks/use-field-dirty.tsx +0 -12
  31. package/src/admin/components/form/hooks/use-field-error.tsx +0 -34
  32. package/src/admin/components/form/utils/get-border-class.ts +0 -22
  33. package/src/admin/i18n/admin-i18n.ts +0 -21
  34. package/src/admin/i18n/translations/en.yml +0 -50
  35. package/src/admin/i18n/translations.ts +0 -5
  36. package/src/admin/pages/AdminRoute.astro +0 -14
  37. package/src/admin/pages/media/EditForm.tsx +0 -136
  38. package/src/admin/pages/media/EditRoute.astro +0 -100
  39. package/src/admin/pages/media/fields/Authors.tsx +0 -44
  40. package/src/admin/pages/media/fields/Categories.tsx +0 -49
  41. package/src/admin/pages/media/fields/Collections.tsx +0 -68
  42. package/src/admin/pages/media/fields/Content.tsx +0 -150
  43. package/src/admin/pages/media/fields/Image.tsx +0 -119
  44. package/src/admin/pages/media/file-system.ts +0 -41
  45. package/src/admin/pages/media/media-item-store.ts +0 -61
  46. package/src/admin/types/media-item.ts +0 -81
  47. package/src/api/media/[mediaId].ts +0 -16
@@ -4,15 +4,21 @@ This autogenerated report shows all built-in languages and the current status of
4
4
 
5
5
  ## **AR** ([ar.yml](./ar.yml))
6
6
 
7
- All keys have been translated. ✅
7
+ Missing keys:
8
+
9
+ - ln.details.edit
8
10
 
9
11
  ## **BN** ([bn.yml](./bn.yml))
10
12
 
11
- All keys have been translated. ✅
13
+ Missing keys:
14
+
15
+ - ln.details.edit
12
16
 
13
17
  ## **DE** ([de.yml](./de.yml))
14
18
 
15
- All keys have been translated. ✅
19
+ Missing keys:
20
+
21
+ - ln.details.edit
16
22
 
17
23
  ## **EN** ([en.yml](./en.yml))
18
24
 
@@ -20,19 +26,27 @@ All keys have been translated. ✅
20
26
 
21
27
  ## **ES** ([es.yml](./es.yml))
22
28
 
23
- All keys have been translated. ✅
29
+ Missing keys:
30
+
31
+ - ln.details.edit
24
32
 
25
33
  ## **FI** ([fi.yml](./fi.yml))
26
34
 
27
- All keys have been translated. ✅
35
+ Missing keys:
36
+
37
+ - ln.details.edit
28
38
 
29
39
  ## **FR** ([fr.yml](./fr.yml))
30
40
 
31
- All keys have been translated. ✅
41
+ Missing keys:
42
+
43
+ - ln.details.edit
32
44
 
33
45
  ## **HI** ([hi.yml](./hi.yml))
34
46
 
35
- All keys have been translated. ✅
47
+ Missing keys:
48
+
49
+ - ln.details.edit
36
50
 
37
51
  ## **KK** ([kk.yml](./kk.yml))
38
52
 
@@ -40,23 +54,34 @@ Missing keys:
40
54
 
41
55
  - ln.previous
42
56
  - ln.next
57
+ - ln.details.edit
43
58
 
44
59
  ## **PT** ([pt.yml](./pt.yml))
45
60
 
46
- All keys have been translated. ✅
61
+ Missing keys:
62
+
63
+ - ln.details.edit
47
64
 
48
65
  ## **RU** ([ru.yml](./ru.yml))
49
66
 
50
- All keys have been translated. ✅
67
+ Missing keys:
68
+
69
+ - ln.details.edit
51
70
 
52
71
  ## **UK** ([uk.yml](./uk.yml))
53
72
 
54
- All keys have been translated. ✅
73
+ Missing keys:
74
+
75
+ - ln.details.edit
55
76
 
56
77
  ## **UR** ([ur.yml](./ur.yml))
57
78
 
58
- All keys have been translated. ✅
79
+ Missing keys:
80
+
81
+ - ln.details.edit
59
82
 
60
83
  ## **ZH** ([zh.yml](./zh.yml))
61
84
 
62
- All keys have been translated. ✅
85
+ Missing keys:
86
+
87
+ - ln.details.edit
@@ -113,6 +113,11 @@ ln.details.open: Open
113
113
  # Used on: https://sk8-ministries.pages.dev/en/media/ollie-with-integrity--en/
114
114
  ln.details.share: Share
115
115
 
116
+ # Button label to start editing an item
117
+ #
118
+ # English: Edit
119
+ ln.details.edit: Edit
120
+
116
121
  # Label for indicating that the media item belongs to a collection.
117
122
  #
118
123
  # English: Part of collection
@@ -1,10 +1,5 @@
1
1
  import YAML from "yaml"
2
2
 
3
- import {
4
- type AdminTranslationKey,
5
- builtInAdminTranslations,
6
- } from "../admin/i18n/translations"
7
-
8
3
  const builtInTranslations = {
9
4
  ar: () => import("./translations/ar.yml?raw"),
10
5
  bn: () => import("./translations/bn.yml?raw"),
@@ -39,7 +34,6 @@ const userTranslations = Object.fromEntries(
39
34
 
40
35
  export const loadTranslations = async (bcp47: string) => ({
41
36
  ...(await loadBuiltInTranslations(builtInTranslations, bcp47)),
42
- ...(await loadBuiltInTranslations(builtInAdminTranslations, bcp47)),
43
37
  ...(await loadUserTranslations(bcp47)),
44
38
  })
45
39
 
@@ -95,4 +89,3 @@ export type LightNetTranslationKey =
95
89
  | "ln.search.title"
96
90
  | "ln.share.url-copied-to-clipboard"
97
91
  | "ln.footer.powered-by-lightnet"
98
- | AdminTranslationKey
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import "./global.css"
3
+
2
4
  import CustomFooter from "virtual:lightnet/components/CustomFooter"
3
5
  import CustomHead from "virtual:lightnet/components/CustomHead"
4
6
  import config from "virtual:lightnet/config"
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -1,6 +1,4 @@
1
1
  ---
2
- import config from "virtual:lightnet/config"
3
-
4
2
  import Icon from "../../../../components/Icon"
5
3
 
6
4
  interface Props {
@@ -13,10 +11,10 @@ const { mediaId } = Astro.props
13
11
  <a
14
12
  class="hidden cursor-pointer items-center gap-2 font-bold text-gray-700 underline"
15
13
  id="edit-btn"
16
- data-admin-enabled={config.experimental?.admin?.enabled}
14
+ data-admin-enabled={false}
17
15
  href={`/admin/media/${mediaId}`}
18
16
  ><Icon className="mdi--square-edit-outline" ariaLabel="" />
19
- {Astro.locals.i18n.t("ln.admin.edit")}</a
17
+ {Astro.locals.i18n.t("ln.details.edit")}</a
20
18
  >
21
19
  <script>
22
20
  const btn: HTMLAnchorElement | null = document.querySelector("#edit-btn")
@@ -48,7 +48,7 @@ export default function SearchFilter({
48
48
  <label className="mb-2 flex items-center gap-2 rounded-2xl border border-gray-300 bg-white px-4 py-3 shadow-inner outline outline-2 outline-offset-2 outline-transparent transition-all ease-in-out focus-within:outline-gray-300">
49
49
  <input
50
50
  type="search"
51
- className="grow placeholder-gray-500 focus:outline-none"
51
+ className="grow bg-white placeholder-gray-500 focus:outline-none"
52
52
  name="search"
53
53
  ref={searchInput}
54
54
  placeholder={t("ln.search.placeholder")}
@@ -1,53 +0,0 @@
1
- import { createWriteStream } from "node:fs"
2
- import { mkdir, rename, rm } from "node:fs/promises"
3
- import { dirname, isAbsolute, relative, resolve } from "node:path"
4
- import { Writable } from "node:stream"
5
- import { fileURLToPath } from "node:url"
6
-
7
- import type { APIRoute } from "astro"
8
- import { root } from "astro:config/server"
9
-
10
- export const prerender = false
11
-
12
- export const POST: APIRoute = async ({ request }) => {
13
- const rootDirPath = fileURLToPath(root)
14
- const requestedPath = new URL(request.url).searchParams.get("path")
15
- if (!requestedPath) {
16
- throw new Error("'path' search param is undefined.")
17
- }
18
- if (isAbsolute(requestedPath)) {
19
- throw new Error("Absolute paths are not allowed.")
20
- }
21
-
22
- const targetPath = resolve(rootDirPath, requestedPath)
23
- const relativeToRoot = relative(rootDirPath, targetPath)
24
- if (
25
- relativeToRoot.startsWith("..") ||
26
- relativeToRoot === "" ||
27
- isAbsolute(relativeToRoot)
28
- ) {
29
- throw new Error("Path escapes project root.")
30
- }
31
- const { body } = request
32
- if (!body) {
33
- throw new Error("Request body missing.")
34
- }
35
-
36
- const targetDir = dirname(targetPath)
37
- await mkdir(targetDir, { recursive: true })
38
-
39
- const tmpPath = `${targetPath}.tmp-${Date.now()}`
40
- try {
41
- await body.pipeTo(Writable.toWeb(createWriteStream(tmpPath)))
42
- await rename(tmpPath, targetPath)
43
- } finally {
44
- await rm(tmpPath, { force: true }).catch(() => {})
45
- }
46
-
47
- return new Response(JSON.stringify({ status: "ok" }), {
48
- status: 200,
49
- headers: {
50
- "Content-Type": "application/json",
51
- },
52
- })
53
- }
@@ -1,129 +0,0 @@
1
- import type { ReactNode } from "react"
2
- import {
3
- type ArrayPath,
4
- type Control,
5
- type FieldValues,
6
- useFieldArray,
7
- type UseFieldArrayAppend,
8
- } from "react-hook-form"
9
-
10
- import Icon from "../../../components/Icon"
11
- import { useI18n } from "../../../i18n/react/use-i18n"
12
- import ErrorMessage from "./atoms/ErrorMessage"
13
- import Hint from "./atoms/Hint"
14
- import Label from "./atoms/Label"
15
- import { useFieldDirty } from "./hooks/use-field-dirty"
16
- import { useFieldError } from "./hooks/use-field-error"
17
- import { getBorderClass } from "./utils/get-border-class"
18
-
19
- export default function DynamicArray<TFieldValues extends FieldValues>({
20
- control,
21
- name,
22
- label,
23
- required = false,
24
- hint,
25
- renderElement,
26
- renderElementMeta,
27
- renderAddButton,
28
- }: {
29
- name: ArrayPath<TFieldValues>
30
- required?: boolean
31
- label: string
32
- hint?: string
33
- control: Control<TFieldValues>
34
- renderElement: (index: number) => ReactNode
35
- renderElementMeta?: (index: number) => ReactNode
36
- renderAddButton: (args: {
37
- addElement: UseFieldArrayAppend<TFieldValues, ArrayPath<TFieldValues>>
38
- index: number
39
- }) => ReactNode
40
- }) {
41
- const { fields, append, remove, swap } = useFieldArray({
42
- name,
43
- control,
44
- })
45
- const errorMessage = useFieldError({ control, name })
46
- const isDirty = useFieldDirty({ control, name })
47
- return (
48
- <fieldset key={name}>
49
- <legend>
50
- <Label
51
- required={required}
52
- label={label}
53
- isDirty={isDirty}
54
- isInvalid={!!errorMessage}
55
- />
56
- </legend>
57
-
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
- >
61
- {fields.map((field, index) => (
62
- <div
63
- className="w-full gap-2 rounded-xl bg-slate-50 px-2 pb-4 shadow-sm"
64
- key={field.id}
65
- >
66
- <div
67
- className={`flex items-center ${renderElementMeta ? "justify-between" : "justify-end"}`}
68
- >
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)}
93
- </div>
94
- ))}
95
- <div className="flex flex-col items-center py-2">
96
- {renderAddButton({ addElement: append, index: fields.length })}
97
- </div>
98
- </div>
99
- <ErrorMessage message={errorMessage} />
100
- <Hint preserveSpace={true} label={hint} />
101
- </fieldset>
102
- )
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,68 +0,0 @@
1
- import type { InputHTMLAttributes } from "react"
2
- import {
3
- type Control,
4
- type FieldValues,
5
- type Path,
6
- type RegisterOptions,
7
- } from "react-hook-form"
8
-
9
- import ErrorMessage from "./atoms/ErrorMessage"
10
- import Hint from "./atoms/Hint"
11
- import Label from "./atoms/Label"
12
- import { useFieldDirty } from "./hooks/use-field-dirty"
13
- import { useFieldError } from "./hooks/use-field-error"
14
- import { getBorderClass } from "./utils/get-border-class"
15
-
16
- type Props<TFieldValues extends FieldValues> = {
17
- name: Path<TFieldValues>
18
- required?: boolean
19
- label?: string
20
- labelSize?: "small" | "medium"
21
- hint?: string
22
- preserveHintSpace?: boolean
23
- control: Control<TFieldValues>
24
- registerOptions?: RegisterOptions<TFieldValues>
25
- } & InputHTMLAttributes<HTMLInputElement>
26
-
27
- export default function Input<TFieldValues extends FieldValues>({
28
- name,
29
- label,
30
- labelSize,
31
- hint,
32
- preserveHintSpace = true,
33
- control,
34
- className,
35
- required = false,
36
- registerOptions,
37
- ...inputProps
38
- }: Props<TFieldValues>) {
39
- const isDirty = useFieldDirty({ control, name })
40
- const errorMessage = useFieldError({ control, name })
41
-
42
- return (
43
- <div key={name} className="group flex w-full flex-col">
44
- {label && (
45
- <label htmlFor={name}>
46
- <Label
47
- label={label}
48
- size={labelSize}
49
- isDirty={isDirty}
50
- isInvalid={!!errorMessage}
51
- required={required}
52
- />
53
- </label>
54
- )}
55
-
56
- <input
57
- className={`rounded-xl ${getBorderClass({ isDirty, errorMessage })} px-4 py-3 shadow-inner disabled:text-slate-500 ${label ? "rounded-ss-none" : ""} ${className}`}
58
- id={name}
59
- aria-invalid={!!errorMessage}
60
- aria-required={required}
61
- {...control.register(name, registerOptions)}
62
- {...inputProps}
63
- />
64
- <ErrorMessage message={errorMessage} />
65
- <Hint preserveSpace={preserveHintSpace} label={hint} />
66
- </div>
67
- )
68
- }
@@ -1,109 +0,0 @@
1
- import "@mdxeditor/editor/style.css"
2
-
3
- import {
4
- BlockTypeSelect,
5
- BoldItalicUnderlineToggles,
6
- type CodeBlockEditorProps,
7
- codeBlockPlugin,
8
- CreateLink,
9
- diffSourcePlugin,
10
- DiffSourceToggleWrapper,
11
- headingsPlugin,
12
- linkDialogPlugin,
13
- linkPlugin,
14
- listsPlugin,
15
- ListsToggle,
16
- markdownShortcutPlugin,
17
- MDXEditor,
18
- quotePlugin,
19
- toolbarPlugin,
20
- UndoRedo,
21
- useCodeBlockEditorContext,
22
- } from "@mdxeditor/editor"
23
- import {
24
- type Control,
25
- Controller,
26
- type FieldValues,
27
- get,
28
- type Path,
29
- } from "react-hook-form"
30
-
31
- /**
32
- * IMPORTANT: Do not import this component directly. It is
33
- * very big. Use it with React lazy loading.
34
- */
35
- export default function LazyLoadedMarkdownEditor<
36
- TFieldValues extends FieldValues,
37
- >({
38
- control,
39
- name,
40
- }: {
41
- name: Path<TFieldValues>
42
- control: Control<TFieldValues>
43
- }) {
44
- return (
45
- <Controller
46
- control={control}
47
- name={name}
48
- render={({
49
- field: { onBlur, onChange, value, ref },
50
- formState: { defaultValues },
51
- }) => (
52
- <MDXEditor
53
- markdown={value ?? ""}
54
- onBlur={onBlur}
55
- onChange={onChange}
56
- contentEditableClassName="prose bg-slate-50 h-80 w-full max-w-full overflow-y-auto"
57
- ref={ref}
58
- onError={(error) =>
59
- console.error("Error while editing markdown", error)
60
- }
61
- plugins={[
62
- headingsPlugin(),
63
- listsPlugin(),
64
- linkPlugin(),
65
- codeBlockPlugin({
66
- codeBlockEditorDescriptors: [
67
- {
68
- match: () => true,
69
- priority: 0,
70
- Editor: TextAreaCodeEditor,
71
- },
72
- ],
73
- }),
74
- linkDialogPlugin(),
75
- quotePlugin(),
76
- markdownShortcutPlugin(),
77
- diffSourcePlugin({ diffMarkdown: get(defaultValues, name) }),
78
- toolbarPlugin({
79
- toolbarContents: () => (
80
- <DiffSourceToggleWrapper>
81
- <UndoRedo />
82
- <BoldItalicUnderlineToggles />
83
- <BlockTypeSelect />
84
- <ListsToggle options={["bullet", "number"]} />
85
- <CreateLink />
86
- </DiffSourceToggleWrapper>
87
- ),
88
- toolbarClassName: "!rounded-none !bg-slate-200",
89
- }),
90
- ]}
91
- />
92
- )}
93
- />
94
- )
95
- }
96
-
97
- function TextAreaCodeEditor(props: CodeBlockEditorProps) {
98
- const cb = useCodeBlockEditorContext()
99
- return (
100
- <div onKeyDown={(e) => e.nativeEvent.stopImmediatePropagation()}>
101
- <textarea
102
- rows={3}
103
- cols={20}
104
- defaultValue={props.code}
105
- onChange={(e) => cb.setCode(e.target.value)}
106
- />
107
- </div>
108
- )
109
- }
@@ -1,62 +0,0 @@
1
- import { lazy, Suspense } from "react"
2
- import { type Control, type FieldValues, type Path } from "react-hook-form"
3
-
4
- import ErrorMessage from "./atoms/ErrorMessage"
5
- import Hint from "./atoms/Hint"
6
- import Label from "./atoms/Label"
7
- import { useFieldDirty } from "./hooks/use-field-dirty"
8
- import { useFieldError } from "./hooks/use-field-error"
9
- import { getBorderClass } from "./utils/get-border-class"
10
-
11
- const LazyLoadedMarkdownEditor = lazy(
12
- () => import("./LazyLoadedMarkdownEditor"),
13
- )
14
-
15
- export default function MarkdownEditor<TFieldValues extends FieldValues>({
16
- control,
17
- name,
18
- required = false,
19
- label,
20
- hint,
21
- }: {
22
- name: Path<TFieldValues>
23
- label: string
24
- required?: boolean
25
- hint?: string
26
- control: Control<TFieldValues>
27
- }) {
28
- const isDirty = useFieldDirty({ control, name })
29
- const errorMessage = useFieldError({ control, name })
30
-
31
- return (
32
- <fieldset key={name} className="group">
33
- <legend>
34
- <Label
35
- required={required}
36
- label={label}
37
- isDirty={isDirty}
38
- isInvalid={!!errorMessage}
39
- />
40
- </legend>
41
-
42
- <div
43
- className={`overflow-hidden rounded-xl rounded-ss-none ${getBorderClass({ isDirty, errorMessage, focusWithin: true })} shadow-sm`}
44
- >
45
- <Suspense
46
- fallback={
47
- <div className="h-[22.75rem] w-full bg-slate-50">
48
- <div className="h-10 bg-slate-100"></div>
49
- </div>
50
- }
51
- >
52
- <LazyLoadedMarkdownEditor
53
- control={control as Control<any>}
54
- name={name}
55
- />
56
- </Suspense>
57
- </div>
58
- <ErrorMessage message={errorMessage} />
59
- <Hint preserveSpace={true} label={hint} />
60
- </fieldset>
61
- )
62
- }
@@ -1,71 +0,0 @@
1
- import { type Control, type FieldValues, type Path } from "react-hook-form"
2
-
3
- import Icon from "../../../components/Icon"
4
- import ErrorMessage from "./atoms/ErrorMessage"
5
- import Hint from "./atoms/Hint"
6
- import Label from "./atoms/Label"
7
- import { useFieldDirty } from "./hooks/use-field-dirty"
8
- import { useFieldError } from "./hooks/use-field-error"
9
- import { getBorderClass } from "./utils/get-border-class"
10
-
11
- export default function Select<TFieldValues extends FieldValues>({
12
- name,
13
- label,
14
- labelSize,
15
- required = false,
16
- control,
17
- defaultValue,
18
- hint,
19
- preserveHintSpace = true,
20
- options,
21
- }: {
22
- name: Path<TFieldValues>
23
- label?: string
24
- labelSize?: "small" | "medium"
25
- hint?: string
26
- required?: boolean
27
- preserveHintSpace?: boolean
28
- defaultValue?: string
29
- control: Control<TFieldValues>
30
- options: { id: string; labelText?: string }[]
31
- }) {
32
- const isDirty = useFieldDirty({ control, name })
33
- const errorMessage = useFieldError({ control, name })
34
- return (
35
- <div key={name} className="group flex w-full flex-col">
36
- {label && (
37
- <label htmlFor={name}>
38
- <Label
39
- label={label}
40
- size={labelSize}
41
- isDirty={isDirty}
42
- required={required}
43
- isInvalid={!!errorMessage}
44
- />
45
- </label>
46
- )}
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>
67
- <ErrorMessage message={errorMessage} />
68
- <Hint preserveSpace={preserveHintSpace} label={hint} />
69
- </div>
70
- )
71
- }