@proef/common-ui 2.2.0 → 2.2.1

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 (32) hide show
  1. package/dist/esm/index.js +26 -3
  2. package/dist/esm/index.js.map +1 -1
  3. package/dist/esm/legacy/components/molecules/DataTable/DataTable.js +1 -1
  4. package/dist/esm/legacy/components/molecules/DataTable/DataTable.js.map +1 -1
  5. package/dist/esm/v1/capabilities/documents/QRScanning/scanQRCodes.js.map +1 -1
  6. package/dist/esm/v1/components/documents/DocumentDropzone/DocumentDropzone.js.map +1 -1
  7. package/dist/esm/v1/components/lists/ListFinder/ListFinder.js +111 -54
  8. package/dist/esm/v1/components/lists/ListFinder/ListFinder.js.map +1 -1
  9. package/dist/esm/v1/foundations/icons/icons.js +49 -0
  10. package/dist/esm/v1/foundations/icons/icons.js.map +1 -1
  11. package/dist/esm/v1/primitives/display/RichText/RichText.js +12 -0
  12. package/dist/esm/v1/primitives/display/RichText/RichText.js.map +1 -0
  13. package/dist/esm/v1/primitives/display/RichText/utils.js +56 -0
  14. package/dist/esm/v1/primitives/display/RichText/utils.js.map +1 -0
  15. package/dist/esm/v1/primitives/documents/PdfPreview/PdfPreview.js.map +1 -1
  16. package/dist/esm/v1/primitives/documents/PdfPreview/pdfjs.js.map +1 -1
  17. package/dist/esm/v1/primitives/forms/RichTextEditor/RichTextEditor.js +253 -0
  18. package/dist/esm/v1/primitives/forms/RichTextEditor/RichTextEditor.js.map +1 -0
  19. package/dist/esm/v1/primitives/forms/TextInput/TextInput.js.map +1 -1
  20. package/dist/esm/v1/primitives/lists/ListItem/ListItem.js +45 -11
  21. package/dist/esm/v1/primitives/lists/ListItem/ListItem.js.map +1 -1
  22. package/dist/style.components.css +1 -1
  23. package/dist/style.css +1 -1
  24. package/dist/style.layered.css +1 -1
  25. package/dist/types/v1/components/lists/ListFinder/ListFinder.d.ts +31 -5
  26. package/dist/types/v1/foundations/icons/icons.d.ts +20 -0
  27. package/dist/types/v1/index.d.ts +8 -3
  28. package/dist/types/v1/primitives/display/RichText/RichText.d.ts +17 -0
  29. package/dist/types/v1/primitives/display/RichText/utils.d.ts +22 -0
  30. package/dist/types/v1/primitives/forms/RichTextEditor/RichTextEditor.d.ts +64 -0
  31. package/dist/types/v1/primitives/lists/ListItem/ListItem.d.ts +39 -8
  32. package/package.json +6 -1
@@ -0,0 +1,253 @@
1
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
+ import { useRef, useState, useCallback, useEffect } from "react";
3
+ import { useEditor, Extension, useEditorState, EditorContent } from "@tiptap/react";
4
+ import StarterKit from "@tiptap/starter-kit";
5
+ import Image from "@tiptap/extension-image";
6
+ import { useField } from "../field.js";
7
+ import { richTextContentClasses, isRichText, textToRichText, richTextIsEmpty } from "../../display/RichText/utils.js";
8
+ import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, ListBulletIcon, ListNumberedIcon, QuoteIcon, ImageIcon, MinimizeIcon, MaximizeIcon } from "../../../foundations/icons/icons.js";
9
+ const DEFAULT_LABELS = {
10
+ bold: "Negrito (Ctrl+B)",
11
+ italic: "Itálico (Ctrl+I)",
12
+ underline: "Sublinhado (Ctrl+U)",
13
+ strike: "Riscado",
14
+ bulletList: "Lista com marcas",
15
+ orderedList: "Lista numerada",
16
+ quote: "Citação",
17
+ image: "Inserir imagem",
18
+ maximize: "Maximizar",
19
+ restore: "Restaurar"
20
+ };
21
+ function imageFiles(data) {
22
+ return Array.from(data?.files ?? []).filter((f) => f.type.startsWith("image/"));
23
+ }
24
+ function editorValue(editor) {
25
+ const html = editor.getHTML();
26
+ return richTextIsEmpty(html) ? "" : html;
27
+ }
28
+ function ToolbarButton({ label, active, disabled, onClick, children }) {
29
+ return /* @__PURE__ */ jsx(
30
+ "button",
31
+ {
32
+ type: "button",
33
+ "aria-label": label,
34
+ title: label,
35
+ "aria-pressed": active === void 0 ? void 0 : active,
36
+ disabled,
37
+ onMouseDown: (e) => e.preventDefault(),
38
+ onClick,
39
+ className: "inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 " + (active ? "bg-primary/10 text-primary dark:bg-slate-700 dark:text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground dark:hover:bg-slate-800"),
40
+ children
41
+ }
42
+ );
43
+ }
44
+ const Separator = () => /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-1 h-4 w-px bg-border" });
45
+ function RichTextEditor({
46
+ value,
47
+ onChange,
48
+ placeholder,
49
+ invalid,
50
+ disabled,
51
+ minHeight = 80,
52
+ maxHeight,
53
+ onUploadImage,
54
+ maximizable,
55
+ onSubmit,
56
+ submitOnEnter,
57
+ autoFocus,
58
+ "aria-label": ariaLabel,
59
+ id,
60
+ className = "",
61
+ labels: labelsProp
62
+ }) {
63
+ const labels = { ...DEFAULT_LABELS, ...labelsProp };
64
+ const field = useField();
65
+ const fileInputRef = useRef(null);
66
+ const [maximized, setMaximized] = useState(false);
67
+ const onChangeRef = useRef(onChange);
68
+ onChangeRef.current = onChange;
69
+ const uploadRef = useRef(onUploadImage);
70
+ uploadRef.current = onUploadImage;
71
+ const submitRef = useRef({ onSubmit, submitOnEnter });
72
+ submitRef.current = { onSubmit, submitOnEnter };
73
+ const emitted = useRef(null);
74
+ const editorRef = useRef(null);
75
+ const insertImages = useCallback(async (files) => {
76
+ for (const file of files) {
77
+ try {
78
+ const src = await uploadRef.current(file);
79
+ editorRef.current?.chain().focus().setImage({ src }).run();
80
+ } catch {
81
+ }
82
+ }
83
+ }, []);
84
+ const editor = useEditor({
85
+ extensions: [
86
+ StarterKit.configure({
87
+ heading: false,
88
+ codeBlock: false,
89
+ horizontalRule: false,
90
+ link: { openOnClick: false, autolink: true, defaultProtocol: "https" }
91
+ }),
92
+ Image.configure({ inline: false, allowBase64: true }),
93
+ Extension.create({
94
+ name: "submitShortcut",
95
+ addKeyboardShortcuts() {
96
+ const submit = () => {
97
+ const { onSubmit: send } = submitRef.current;
98
+ if (!send) return false;
99
+ send();
100
+ return true;
101
+ };
102
+ return {
103
+ "Mod-Enter": submit,
104
+ Enter: ({ editor: ed }) => {
105
+ if (!submitRef.current.submitOnEnter) return false;
106
+ if (ed.isActive("bulletList") || ed.isActive("orderedList")) return false;
107
+ return submit();
108
+ }
109
+ };
110
+ }
111
+ })
112
+ ],
113
+ content: isRichText(value) ? value : textToRichText(value),
114
+ editable: !disabled,
115
+ autofocus: autoFocus ? "end" : false,
116
+ onUpdate: ({ editor: ed }) => {
117
+ const html = editorValue(ed);
118
+ emitted.current = html;
119
+ onChangeRef.current(html);
120
+ },
121
+ editorProps: {
122
+ attributes: {
123
+ class: `px-2.5 py-2 text-control leading-normal text-foreground outline-none ${richTextContentClasses}`,
124
+ role: "textbox",
125
+ "aria-multiline": "true"
126
+ },
127
+ // Colar/largar imagens só onde há sítio para as guardar.
128
+ handlePaste: (_view, event) => {
129
+ if (!uploadRef.current) return false;
130
+ const files = imageFiles(event.clipboardData);
131
+ if (!files.length) return false;
132
+ event.preventDefault();
133
+ event.stopPropagation();
134
+ void insertImages(files);
135
+ return true;
136
+ },
137
+ handleDrop: (_view, event) => {
138
+ if (!uploadRef.current) return false;
139
+ const files = imageFiles(event.dataTransfer);
140
+ if (!files.length) return false;
141
+ event.preventDefault();
142
+ event.stopPropagation();
143
+ void insertImages(files);
144
+ return true;
145
+ }
146
+ }
147
+ });
148
+ editorRef.current = editor;
149
+ const state = useEditorState({
150
+ editor,
151
+ selector: ({ editor: ed }) => ed ? {
152
+ empty: ed.isEmpty,
153
+ bold: ed.isActive("bold"),
154
+ italic: ed.isActive("italic"),
155
+ underline: ed.isActive("underline"),
156
+ strike: ed.isActive("strike"),
157
+ bulletList: ed.isActive("bulletList"),
158
+ orderedList: ed.isActive("orderedList"),
159
+ quote: ed.isActive("blockquote")
160
+ } : null
161
+ });
162
+ useEffect(() => {
163
+ if (!editor || value === emitted.current) return;
164
+ if (!value && editor.isEmpty) return;
165
+ editor.commands.setContent(isRichText(value) ? value : textToRichText(value), { emitUpdate: false });
166
+ emitted.current = value;
167
+ }, [value, editor]);
168
+ useEffect(() => {
169
+ editor?.setEditable(!disabled);
170
+ }, [editor, disabled]);
171
+ useEffect(() => {
172
+ if (!editor) return;
173
+ const el = editor.view.dom;
174
+ const set = (name, v) => v ? el.setAttribute(name, v) : el.removeAttribute(name);
175
+ set("id", id ?? field.id);
176
+ set("aria-label", ariaLabel);
177
+ set("aria-invalid", invalid ? "true" : void 0);
178
+ set("aria-required", field.required ? "true" : void 0);
179
+ set("aria-disabled", disabled ? "true" : void 0);
180
+ }, [editor, id, field.id, field.required, ariaLabel, invalid, disabled]);
181
+ useEffect(() => {
182
+ if (!maximized) return;
183
+ const onKey = (e) => {
184
+ if (e.key === "Escape") {
185
+ e.stopPropagation();
186
+ setMaximized(false);
187
+ }
188
+ };
189
+ window.addEventListener("keydown", onKey, true);
190
+ return () => window.removeEventListener("keydown", onKey, true);
191
+ }, [maximized]);
192
+ if (!editor || !state) return null;
193
+ const run = (fn) => () => fn(editor.chain().focus()).run();
194
+ const writeArea = maximized ? {} : { minHeight, maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight };
195
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
196
+ maximized && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[60] bg-black/40 backdrop-blur-sm", onClick: () => setMaximized(false) }),
197
+ /* @__PURE__ */ jsxs(
198
+ "div",
199
+ {
200
+ className: "overflow-clip rounded-lg border bg-card transition-[border-color,box-shadow] duration-150 dark:bg-slate-900 " + (invalid ? "border-destructive ring-3 ring-destructive/15 " : "border-border focus-within:border-primary/40 focus-within:ring-3 focus-within:ring-primary/10 dark:border-slate-600 ") + (disabled ? "bg-muted opacity-70 " : "") + (maximized ? "fixed inset-4 z-[61] flex flex-col shadow-2xl md:inset-10 " : "w-full min-w-0 ") + className,
201
+ children: [
202
+ /* @__PURE__ */ jsxs("div", { role: "toolbar", "aria-label": "Formatação", className: "sticky top-0 z-10 flex flex-wrap items-center gap-0.5 border-b border-border bg-muted/40 px-1.5 py-1 dark:border-slate-700 dark:bg-slate-800/60", children: [
203
+ /* @__PURE__ */ jsx(ToolbarButton, { label: labels.bold, active: state.bold, disabled, onClick: run((c) => c.toggleBold()), children: /* @__PURE__ */ jsx(BoldIcon, { size: 14 }) }),
204
+ /* @__PURE__ */ jsx(ToolbarButton, { label: labels.italic, active: state.italic, disabled, onClick: run((c) => c.toggleItalic()), children: /* @__PURE__ */ jsx(ItalicIcon, { size: 14 }) }),
205
+ /* @__PURE__ */ jsx(ToolbarButton, { label: labels.underline, active: state.underline, disabled, onClick: run((c) => c.toggleUnderline()), children: /* @__PURE__ */ jsx(UnderlineIcon, { size: 14 }) }),
206
+ /* @__PURE__ */ jsx(ToolbarButton, { label: labels.strike, active: state.strike, disabled, onClick: run((c) => c.toggleStrike()), children: /* @__PURE__ */ jsx(StrikethroughIcon, { size: 14 }) }),
207
+ /* @__PURE__ */ jsx(Separator, {}),
208
+ /* @__PURE__ */ jsx(ToolbarButton, { label: labels.bulletList, active: state.bulletList, disabled, onClick: run((c) => c.toggleBulletList()), children: /* @__PURE__ */ jsx(ListBulletIcon, { size: 14 }) }),
209
+ /* @__PURE__ */ jsx(ToolbarButton, { label: labels.orderedList, active: state.orderedList, disabled, onClick: run((c) => c.toggleOrderedList()), children: /* @__PURE__ */ jsx(ListNumberedIcon, { size: 14 }) }),
210
+ /* @__PURE__ */ jsx(ToolbarButton, { label: labels.quote, active: state.quote, disabled, onClick: run((c) => c.toggleBlockquote()), children: /* @__PURE__ */ jsx(QuoteIcon, { size: 14 }) }),
211
+ onUploadImage && /* @__PURE__ */ jsxs(Fragment, { children: [
212
+ /* @__PURE__ */ jsx(Separator, {}),
213
+ /* @__PURE__ */ jsx(ToolbarButton, { label: labels.image, disabled, onClick: () => fileInputRef.current?.click(), children: /* @__PURE__ */ jsx(ImageIcon, { size: 14 }) }),
214
+ /* @__PURE__ */ jsx(
215
+ "input",
216
+ {
217
+ ref: fileInputRef,
218
+ type: "file",
219
+ accept: "image/*",
220
+ multiple: true,
221
+ hidden: true,
222
+ onChange: (e) => {
223
+ const files = Array.from(e.target.files ?? []);
224
+ e.target.value = "";
225
+ void insertImages(files);
226
+ }
227
+ }
228
+ )
229
+ ] }),
230
+ maximizable && /* @__PURE__ */ jsx("span", { className: "ml-auto", children: /* @__PURE__ */ jsx(ToolbarButton, { label: maximized ? labels.restore : labels.maximize, onClick: () => setMaximized((m) => !m), children: maximized ? /* @__PURE__ */ jsx(MinimizeIcon, { size: 14 }) : /* @__PURE__ */ jsx(MaximizeIcon, { size: 14 }) }) })
231
+ ] }),
232
+ /* @__PURE__ */ jsxs(
233
+ "div",
234
+ {
235
+ className: `relative cursor-text ${maximized ? "flex-1 overflow-y-auto" : "overflow-y-auto"} [&>div]:h-full [&_.ProseMirror]:min-h-full`,
236
+ style: writeArea,
237
+ onClick: () => !disabled && !editor.isFocused && editor.chain().focus().run(),
238
+ children: [
239
+ state.empty && placeholder && /* @__PURE__ */ jsx("p", { "aria-hidden": true, className: "pointer-events-none absolute top-2 left-2.5 m-0 text-control text-muted-foreground/70 select-none", children: placeholder }),
240
+ /* @__PURE__ */ jsx(EditorContent, { editor, className: "h-full" })
241
+ ]
242
+ }
243
+ )
244
+ ]
245
+ }
246
+ )
247
+ ] });
248
+ }
249
+ export {
250
+ RichTextEditor,
251
+ RichTextEditor as default
252
+ };
253
+ //# sourceMappingURL=RichTextEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RichTextEditor.js","sources":["../../../../../../src/v1/primitives/forms/RichTextEditor/RichTextEditor.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useRef, useState } from 'react'\r\nimport { EditorContent, Extension, useEditor, useEditorState, type Editor } from '@tiptap/react'\r\nimport StarterKit from '@tiptap/starter-kit'\r\nimport Image from '@tiptap/extension-image'\r\nimport { useField } from '../field'\r\nimport { isRichText, richTextContentClasses, richTextIsEmpty, textToRichText } from '../../display/RichText/utils'\r\nimport {\r\n BoldIcon,\r\n ItalicIcon,\r\n UnderlineIcon,\r\n StrikethroughIcon,\r\n ListBulletIcon,\r\n ListNumberedIcon,\r\n QuoteIcon,\r\n ImageIcon,\r\n MaximizeIcon,\r\n MinimizeIcon,\r\n} from '../../../foundations/icons/icons'\r\n\r\nexport interface RichTextEditorLabels {\r\n bold: string\r\n italic: string\r\n underline: string\r\n strike: string\r\n bulletList: string\r\n orderedList: string\r\n quote: string\r\n image: string\r\n maximize: string\r\n restore: string\r\n}\r\n\r\nconst DEFAULT_LABELS: RichTextEditorLabels = {\r\n bold: 'Negrito (Ctrl+B)',\r\n italic: 'Itálico (Ctrl+I)',\r\n underline: 'Sublinhado (Ctrl+U)',\r\n strike: 'Riscado',\r\n bulletList: 'Lista com marcas',\r\n orderedList: 'Lista numerada',\r\n quote: 'Citação',\r\n image: 'Inserir imagem',\r\n maximize: 'Maximizar',\r\n restore: 'Restaurar',\r\n}\r\n\r\nexport interface RichTextEditorProps {\r\n /** O texto, em HTML. Texto simples (antigo, sem formatação) também serve: cada linha vira um parágrafo. */\r\n value: string\r\n /** Recebe o HTML a cada alteração; `''` quando fica vazio (para validar \"obrigatório\" sem olhar para `<p></p>`). */\r\n onChange: (html: string) => void\r\n placeholder?: string\r\n /** Marca o campo a vermelho (ex: obrigatório por preencher ao submeter). */\r\n invalid?: boolean\r\n disabled?: boolean\r\n /** Altura mínima da área de escrita, em px. Por omissão 80. */\r\n minHeight?: number\r\n /** Altura máxima (px ou CSS, ex: `'50vh'`): cresce com o texto até aqui e depois faz scroll por dentro. Sem isto, cresce sempre. */\r\n maxHeight?: number | string\r\n /**\r\n * Onde guardar uma imagem (escolhida na barra, colada ou largada); devolve o `src` a pôr no texto.\r\n * Sem isto, o editor não aceita imagens.\r\n */\r\n onUploadImage?: (file: File) => Promise<string>\r\n /** Mostra na barra o botão para escrever com o editor em ecrã quase inteiro (Esc volta). */\r\n maximizable?: boolean\r\n /**\r\n * Enviar com o teclado (ex: uma conversa): Ctrl+Enter chama-o sempre. Com `submitOnEnter`,\r\n * também o Enter — exceto dentro de uma lista, onde o Enter cria o item seguinte. Shift+Enter muda de linha.\r\n */\r\n onSubmit?: () => void\r\n submitOnEnter?: boolean\r\n autoFocus?: boolean\r\n /** Nome acessível, quando não há etiqueta à volta (ex: \"Escrever comentário\"). */\r\n 'aria-label'?: string\r\n id?: string\r\n /** Aplica-se à caixa à volta. */\r\n className?: string\r\n /** Textos da barra (para outra língua). */\r\n labels?: Partial<RichTextEditorLabels>\r\n}\r\n\r\nfunction imageFiles(data: DataTransfer | null): File[] {\r\n return Array.from(data?.files ?? []).filter((f) => f.type.startsWith('image/'))\r\n}\r\n\r\n/** O HTML que o editor tem, ou `''` se estiver vazio. */\r\nfunction editorValue(editor: Editor): string {\r\n const html = editor.getHTML()\r\n return richTextIsEmpty(html) ? '' : html\r\n}\r\n\r\nfunction ToolbarButton({ label, active, disabled, onClick, children }: {\r\n label: string\r\n active?: boolean\r\n disabled?: boolean\r\n onClick: () => void\r\n children: React.ReactNode\r\n}) {\r\n return (\r\n <button\r\n type=\"button\"\r\n aria-label={label}\r\n title={label}\r\n aria-pressed={active === undefined ? undefined : active}\r\n disabled={disabled}\r\n // Não tira o foco ao editor: a formatação aplica-se ao que está selecionado.\r\n onMouseDown={(e) => e.preventDefault()}\r\n onClick={onClick}\r\n className={\r\n 'inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md transition-colors duration-150 ' +\r\n 'focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 ' +\r\n (active\r\n ? 'bg-primary/10 text-primary dark:bg-slate-700 dark:text-foreground'\r\n : 'text-muted-foreground hover:bg-muted hover:text-foreground dark:hover:bg-slate-800')\r\n }\r\n >\r\n {children}\r\n </button>\r\n )\r\n}\r\n\r\nconst Separator = () => <span aria-hidden className=\"mx-1 h-4 w-px bg-border\" />\r\n\r\n/**\r\n * Texto com formatação, que se vê enquanto se escreve: negrito, itálico, sublinhado, riscado,\r\n * listas, citações e (com `onUploadImage`) imagens. Atalhos habituais: Ctrl+B, Ctrl+I, Ctrl+U;\r\n * \"- \" ou \"1. \" no início da linha começam uma lista.\r\n *\r\n * Guarda HTML. Para o mostrar, usar sempre o [RichText](?path=/docs/v1-primitives-display-richtext--docs),\r\n * que o limpa antes de entrar na página. Para validar \"obrigatório\", o `onChange` dá `''` quando está vazio\r\n * (ou `richTextIsEmpty`).\r\n *\r\n * Para uma conversa: `onSubmit` + `submitOnEnter` (Enter envia, Shift+Enter muda de linha) e `maxHeight`\r\n * para crescer com o texto até um limite.\r\n *\r\n * Dentro de um [FormField](?path=/docs/v1-components-forms-formfield--docs) fica com o `id` do campo.\r\n */\r\nexport function RichTextEditor({\r\n value,\r\n onChange,\r\n placeholder,\r\n invalid,\r\n disabled,\r\n minHeight = 80,\r\n maxHeight,\r\n onUploadImage,\r\n maximizable,\r\n onSubmit,\r\n submitOnEnter,\r\n autoFocus,\r\n 'aria-label': ariaLabel,\r\n id,\r\n className = '',\r\n labels: labelsProp,\r\n}: RichTextEditorProps) {\r\n const labels = { ...DEFAULT_LABELS, ...labelsProp }\r\n const field = useField()\r\n const fileInputRef = useRef<HTMLInputElement>(null)\r\n const [maximized, setMaximized] = useState(false)\r\n\r\n // Em refs: o editor é criado uma vez e os handlers do ProseMirror ficariam presos à primeira versão das props.\r\n const onChangeRef = useRef(onChange)\r\n onChangeRef.current = onChange\r\n const uploadRef = useRef(onUploadImage)\r\n uploadRef.current = onUploadImage\r\n const submitRef = useRef({ onSubmit, submitOnEnter })\r\n submitRef.current = { onSubmit, submitOnEnter }\r\n // O último valor que o editor deu: um `value` igual vem dele e não volta a ser posto lá dentro.\r\n const emitted = useRef<string | null>(null)\r\n const editorRef = useRef<Editor | null>(null)\r\n\r\n const insertImages = useCallback(async (files: File[]) => {\r\n for (const file of files) {\r\n try {\r\n const src = await uploadRef.current!(file)\r\n editorRef.current?.chain().focus().setImage({ src }).run()\r\n } catch {\r\n // Falhar a subir uma imagem não leva o texto já escrito; quem chama mostra o erro pelo seu lado.\r\n }\r\n }\r\n }, [])\r\n\r\n const editor = useEditor({\r\n extensions: [\r\n StarterKit.configure({\r\n heading: false,\r\n codeBlock: false,\r\n horizontalRule: false,\r\n link: { openOnClick: false, autolink: true, defaultProtocol: 'https' },\r\n }),\r\n Image.configure({ inline: false, allowBase64: true }),\r\n Extension.create({\r\n name: 'submitShortcut',\r\n addKeyboardShortcuts() {\r\n const submit = () => {\r\n const { onSubmit: send } = submitRef.current\r\n if (!send) return false\r\n send()\r\n return true\r\n }\r\n return {\r\n 'Mod-Enter': submit,\r\n Enter: ({ editor: ed }) => {\r\n if (!submitRef.current.submitOnEnter) return false\r\n if (ed.isActive('bulletList') || ed.isActive('orderedList')) return false\r\n return submit()\r\n },\r\n }\r\n },\r\n }),\r\n ],\r\n content: isRichText(value) ? value : textToRichText(value),\r\n editable: !disabled,\r\n autofocus: autoFocus ? 'end' : false,\r\n onUpdate: ({ editor: ed }) => {\r\n const html = editorValue(ed)\r\n emitted.current = html\r\n onChangeRef.current(html)\r\n },\r\n editorProps: {\r\n attributes: {\r\n class: `px-2.5 py-2 text-control leading-normal text-foreground outline-none ${richTextContentClasses}`,\r\n role: 'textbox',\r\n 'aria-multiline': 'true',\r\n },\r\n // Colar/largar imagens só onde há sítio para as guardar.\r\n handlePaste: (_view, event) => {\r\n if (!uploadRef.current) return false\r\n const files = imageFiles(event.clipboardData)\r\n if (!files.length) return false\r\n event.preventDefault()\r\n event.stopPropagation()\r\n void insertImages(files)\r\n return true\r\n },\r\n handleDrop: (_view, event) => {\r\n if (!uploadRef.current) return false\r\n const files = imageFiles((event as DragEvent).dataTransfer)\r\n if (!files.length) return false\r\n event.preventDefault()\r\n event.stopPropagation()\r\n void insertImages(files)\r\n return true\r\n },\r\n },\r\n })\r\n editorRef.current = editor\r\n\r\n // A barra só volta a desenhar quando muda o que mostra (o editor não redesenha a cada tecla).\r\n const state = useEditorState({\r\n editor,\r\n selector: ({ editor: ed }) =>\r\n ed\r\n ? {\r\n empty: ed.isEmpty,\r\n bold: ed.isActive('bold'),\r\n italic: ed.isActive('italic'),\r\n underline: ed.isActive('underline'),\r\n strike: ed.isActive('strike'),\r\n bulletList: ed.isActive('bulletList'),\r\n orderedList: ed.isActive('orderedList'),\r\n quote: ed.isActive('blockquote'),\r\n }\r\n : null,\r\n })\r\n\r\n // Valor mudado por fora (ex: limpar depois de enviar, abrir outro registo).\r\n useEffect(() => {\r\n if (!editor || value === emitted.current) return\r\n if (!value && editor.isEmpty) return\r\n editor.commands.setContent(isRichText(value) ? value : textToRichText(value), { emitUpdate: false })\r\n emitted.current = value\r\n }, [value, editor])\r\n\r\n useEffect(() => {\r\n editor?.setEditable(!disabled)\r\n }, [editor, disabled])\r\n\r\n // Atributos que dependem das props, no próprio campo editável.\r\n useEffect(() => {\r\n if (!editor) return\r\n const el = editor.view.dom as HTMLElement\r\n const set = (name: string, v: string | undefined) => (v ? el.setAttribute(name, v) : el.removeAttribute(name))\r\n set('id', id ?? field.id)\r\n set('aria-label', ariaLabel)\r\n set('aria-invalid', invalid ? 'true' : undefined)\r\n set('aria-required', field.required ? 'true' : undefined)\r\n set('aria-disabled', disabled ? 'true' : undefined)\r\n }, [editor, id, field.id, field.required, ariaLabel, invalid, disabled])\r\n\r\n // Esc sai do modo maximizado (e só isso — não fecha a janela à volta).\r\n useEffect(() => {\r\n if (!maximized) return\r\n const onKey = (e: KeyboardEvent) => {\r\n if (e.key === 'Escape') {\r\n e.stopPropagation()\r\n setMaximized(false)\r\n }\r\n }\r\n window.addEventListener('keydown', onKey, true)\r\n return () => window.removeEventListener('keydown', onKey, true)\r\n }, [maximized])\r\n\r\n if (!editor || !state) return null\r\n\r\n const run = (fn: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>) => () => fn(editor.chain().focus()).run()\r\n const writeArea: React.CSSProperties = maximized\r\n ? {}\r\n : { minHeight, maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight }\r\n\r\n return (\r\n <>\r\n {maximized && <div className=\"fixed inset-0 z-[60] bg-black/40 backdrop-blur-sm\" onClick={() => setMaximized(false)} />}\r\n {/* overflow-clip e não overflow-hidden: o hidden faria disto um contentor de scroll e a barra deixava de ficar presa. */}\r\n <div\r\n className={\r\n 'overflow-clip rounded-lg border bg-card transition-[border-color,box-shadow] duration-150 dark:bg-slate-900 ' +\r\n (invalid\r\n ? 'border-destructive ring-3 ring-destructive/15 '\r\n : 'border-border focus-within:border-primary/40 focus-within:ring-3 focus-within:ring-primary/10 dark:border-slate-600 ') +\r\n (disabled ? 'bg-muted opacity-70 ' : '') +\r\n (maximized ? 'fixed inset-4 z-[61] flex flex-col shadow-2xl md:inset-10 ' : 'w-full min-w-0 ') +\r\n className\r\n }\r\n >\r\n <div role=\"toolbar\" aria-label=\"Formatação\" className=\"sticky top-0 z-10 flex flex-wrap items-center gap-0.5 border-b border-border bg-muted/40 px-1.5 py-1 dark:border-slate-700 dark:bg-slate-800/60\">\r\n <ToolbarButton label={labels.bold} active={state.bold} disabled={disabled} onClick={run((c) => c.toggleBold())}>\r\n <BoldIcon size={14} />\r\n </ToolbarButton>\r\n <ToolbarButton label={labels.italic} active={state.italic} disabled={disabled} onClick={run((c) => c.toggleItalic())}>\r\n <ItalicIcon size={14} />\r\n </ToolbarButton>\r\n <ToolbarButton label={labels.underline} active={state.underline} disabled={disabled} onClick={run((c) => c.toggleUnderline())}>\r\n <UnderlineIcon size={14} />\r\n </ToolbarButton>\r\n <ToolbarButton label={labels.strike} active={state.strike} disabled={disabled} onClick={run((c) => c.toggleStrike())}>\r\n <StrikethroughIcon size={14} />\r\n </ToolbarButton>\r\n <Separator />\r\n <ToolbarButton label={labels.bulletList} active={state.bulletList} disabled={disabled} onClick={run((c) => c.toggleBulletList())}>\r\n <ListBulletIcon size={14} />\r\n </ToolbarButton>\r\n <ToolbarButton label={labels.orderedList} active={state.orderedList} disabled={disabled} onClick={run((c) => c.toggleOrderedList())}>\r\n <ListNumberedIcon size={14} />\r\n </ToolbarButton>\r\n <ToolbarButton label={labels.quote} active={state.quote} disabled={disabled} onClick={run((c) => c.toggleBlockquote())}>\r\n <QuoteIcon size={14} />\r\n </ToolbarButton>\r\n {onUploadImage && (\r\n <>\r\n <Separator />\r\n <ToolbarButton label={labels.image} disabled={disabled} onClick={() => fileInputRef.current?.click()}>\r\n <ImageIcon size={14} />\r\n </ToolbarButton>\r\n <input\r\n ref={fileInputRef}\r\n type=\"file\"\r\n accept=\"image/*\"\r\n multiple\r\n hidden\r\n onChange={(e) => {\r\n const files = Array.from(e.target.files ?? [])\r\n e.target.value = ''\r\n void insertImages(files)\r\n }}\r\n />\r\n </>\r\n )}\r\n {maximizable && (\r\n <span className=\"ml-auto\">\r\n <ToolbarButton label={maximized ? labels.restore : labels.maximize} onClick={() => setMaximized((m) => !m)}>\r\n {maximized ? <MinimizeIcon size={14} /> : <MaximizeIcon size={14} />}\r\n </ToolbarButton>\r\n </span>\r\n )}\r\n </div>\r\n {/* A área de escrita: clicar em qualquer ponto dela põe o cursor no editor. */}\r\n <div\r\n className={`relative cursor-text ${maximized ? 'flex-1 overflow-y-auto' : 'overflow-y-auto'} [&>div]:h-full [&_.ProseMirror]:min-h-full`}\r\n style={writeArea}\r\n onClick={() => !disabled && !editor.isFocused && editor.chain().focus().run()}\r\n >\r\n {state.empty && placeholder && (\r\n <p aria-hidden className=\"pointer-events-none absolute top-2 left-2.5 m-0 text-control text-muted-foreground/70 select-none\">\r\n {placeholder}\r\n </p>\r\n )}\r\n <EditorContent editor={editor} className=\"h-full\" />\r\n </div>\r\n </div>\r\n </>\r\n )\r\n}\r\n\r\nexport default RichTextEditor\r\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TextInput.js","sources":["../../../../../../src/v1/primitives/forms/TextInput/TextInput.tsx"],"sourcesContent":["import React, { forwardRef } from 'react'\r\nimport { fieldClasses, readOnlyClasses, useField } from '../field'\r\n\r\nexport interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {\r\n /** Marca o campo a vermelho (ex: obrigatório por preencher ao submeter). */\r\n invalid?: boolean\r\n}\r\n\r\n/**\r\n * Campo de texto de uma linha (também data, número, etc. pelo `type`).\r\n *\r\n * Dentro de um [FormField](?path=/docs/v1-components-forms-formfield--docs) fica\r\n * ligado à etiqueta automaticamente.\r\n *\r\n * **Aceita os atributos HTML do [&lt;input&gt;](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input)**, com o mesmo comportamento: `value`/`onChange`, `required`, `disabled`, `readOnly`, `name`, `placeholder`, `type`, `min`/`max`, `maxLength`, `pattern`, `ref`, ...\r\n * Só os próprios do V1 aparecem na tabela abaixo.\r\n */\r\nconst TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput({ invalid, className = '', id, required, ...props }, ref) {\r\n const field = useField()\r\n return (\r\n <input\r\n ref={ref}\r\n id={id ?? field.id}\r\n required={required ?? field.required}\r\n aria-invalid={invalid || undefined}\r\n className={`h-control ${fieldClasses} ${readOnlyClasses} ${className}`}\r\n {...props}\r\n />\r\n )\r\n})\r\n\r\nexport default TextInput\r\n"],"names":["TextInput"],"mappings":";;;AAiBA,MAAM,YAAY,WAA6C,SAASA,WAAU,EAAE,SAAS,YAAY,IAAI,IAAI,UAAU,GAAG,MAAA,GAAS,KAAK;AAC1I,QAAM,QAAQ,SAAA;AACd,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,IAAI,MAAM,MAAM;AAAA,MAChB,UAAU,YAAY,MAAM;AAAA,MAC5B,gBAAc,WAAW;AAAA,MACzB,WAAW,aAAa,YAAY,IAAI,eAAe,IAAI,SAAS;AAAA,MACnE,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV,CAAC;"}
1
+ {"version":3,"file":"TextInput.js","sources":["../../../../../../src/v1/primitives/forms/TextInput/TextInput.tsx"],"sourcesContent":["import React, { forwardRef } from 'react'\nimport { fieldClasses, readOnlyClasses, useField } from '../field'\n\nexport interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {\n /** Marca o campo a vermelho (ex: obrigatório por preencher ao submeter). */\n invalid?: boolean\n}\n\n/**\n * Campo de texto de uma linha (também data, número, etc. pelo `type`).\n *\n * Dentro de um [FormField](?path=/docs/v1-components-forms-formfield--docs) fica\n * ligado à etiqueta automaticamente.\n *\n * **Aceita os atributos HTML do [&lt;input&gt;](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input)**, com o mesmo comportamento: `value`/`onChange`, `required`, `disabled`, `readOnly`, `name`, `placeholder`, `type`, `min`/`max`, `maxLength`, `pattern`, `ref`, ...\n * Só os próprios do V1 aparecem na tabela abaixo.\n */\nconst TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput({ invalid, className = '', id, required, ...props }, ref) {\n const field = useField()\n return (\n <input\n ref={ref}\n id={id ?? field.id}\n required={required ?? field.required}\n aria-invalid={invalid || undefined}\n className={`h-control ${fieldClasses} ${readOnlyClasses} ${className}`}\n {...props}\n />\n )\n})\n\nexport default TextInput\n"],"names":["TextInput"],"mappings":";;;AAiBA,MAAM,YAAY,WAA6C,SAASA,WAAU,EAAE,SAAS,YAAY,IAAI,IAAI,UAAU,GAAG,MAAA,GAAS,KAAK;AAC1I,QAAM,QAAQ,SAAA;AACd,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,IAAI,MAAM,MAAM;AAAA,MAChB,UAAU,YAAY,MAAM;AAAA,MAC5B,gBAAc,WAAW;AAAA,MACzB,WAAW,aAAa,YAAY,IAAI,eAAe,IAAI,SAAS;AAAA,MACnE,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV,CAAC;"}
@@ -1,39 +1,73 @@
1
- import { jsxs, jsx } from "react/jsx-runtime";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { Fragment as Fragment$1 } from "react";
2
3
  import Checkbox from "../../forms/Checkbox/Checkbox.js";
4
+ const LINHA_ESBATIDA = "bg-[linear-gradient(to_right,transparent,var(--color-slate-200)_15%,var(--color-slate-200)_85%,transparent)] bg-[length:100%_1px] bg-bottom bg-no-repeat last:bg-none";
5
+ const semAcento = (c) => c.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
6
+ function HighlightText({ text, terms }) {
7
+ const termos = (terms ?? []).map((t) => [...t].map(semAcento).join("")).filter(Boolean);
8
+ if (!termos.length) return /* @__PURE__ */ jsx(Fragment, { children: text });
9
+ const alvo = [...text].map((c) => semAcento(c).charAt(0) || c).join("");
10
+ const marcado = new Array(alvo.length).fill(false);
11
+ for (const t of termos) {
12
+ for (let i = alvo.indexOf(t); i >= 0; i = alvo.indexOf(t, i + 1)) {
13
+ for (let k = i; k < i + t.length; k++) marcado[k] = true;
14
+ }
15
+ }
16
+ if (!marcado.some(Boolean)) return /* @__PURE__ */ jsx(Fragment, { children: text });
17
+ const chars = [...text];
18
+ const partes = [];
19
+ chars.forEach((c, i) => {
20
+ const ultimo = partes[partes.length - 1];
21
+ if (ultimo && ultimo.hit === marcado[i]) ultimo.txt += c;
22
+ else partes.push({ txt: c, hit: marcado[i] });
23
+ });
24
+ return /* @__PURE__ */ jsx(Fragment, { children: partes.map((p, i) => p.hit ? /* @__PURE__ */ jsx("mark", { className: "rounded-sm bg-amber-200 font-bold text-amber-900", children: p.txt }, i) : /* @__PURE__ */ jsx(Fragment$1, { children: p.txt }, i)) });
25
+ }
3
26
  function ListItem({
4
27
  title,
5
28
  code,
6
29
  badges,
7
30
  meta,
31
+ tag,
32
+ aside,
8
33
  active = false,
9
34
  onClick,
10
35
  checked = false,
11
36
  onCheckedChange,
12
37
  checkboxLabel,
38
+ highlight,
13
39
  className = ""
14
40
  }) {
15
- const fundo = active ? "bg-proef-blue/8" : checked ? "bg-slate-50" : "hover:bg-slate-50";
16
- return /* @__PURE__ */ jsxs("li", { className: `v1-reset relative flex list-none items-start gap-2.5 border-b border-slate-100 px-3 py-2.5 transition-colors ${fundo} ${className}`, children: [
17
- active && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-y-0 left-0 w-0.5 bg-proef-blue" }),
18
- onCheckedChange && /* @__PURE__ */ jsx(Checkbox, { checked, onChange: (e) => onCheckedChange(e.target.checked), "aria-label": checkboxLabel, className: "z-10 mt-0.5" }),
41
+ const fundo = active ? "bg-blue-50" : checked ? "bg-proef-turquoise/5" : "hover:bg-slate-50";
42
+ const realcar = (n) => typeof n === "string" ? /* @__PURE__ */ jsx(HighlightText, { text: n, terms: highlight }) : n;
43
+ return /* @__PURE__ */ jsxs("li", { className: `v1-reset relative flex list-none items-start gap-1.5 px-3 py-3 transition-colors ${LINHA_ESBATIDA} ${fundo} ${className}`, children: [
44
+ onCheckedChange && /* @__PURE__ */ jsxs("span", { className: "flex w-7 shrink-0 flex-col items-center gap-1", children: [
45
+ /* @__PURE__ */ jsx(Checkbox, { checked, onChange: (e) => onCheckedChange(e.target.checked), "aria-label": checkboxLabel, className: "z-10 mt-0.5" }),
46
+ tag && /* @__PURE__ */ jsx("span", { className: "text-[8px] leading-none font-semibold text-slate-400", children: tag })
47
+ ] }),
19
48
  /* @__PURE__ */ jsxs(
20
49
  "button",
21
50
  {
22
51
  type: "button",
23
52
  onClick,
24
53
  "aria-current": active || void 0,
25
- className: "min-w-0 flex-1 text-left outline-none after:absolute after:inset-0 focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-primary",
54
+ className: "min-w-0 flex-1 space-y-1 text-left outline-none after:absolute after:inset-0 focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-primary",
26
55
  children: [
27
- badges && /* @__PURE__ */ jsx("span", { className: "mb-1 flex flex-wrap items-center gap-1.5", children: badges }),
28
- code && /* @__PURE__ */ jsx("span", { className: "block font-mono text-micro text-slate-400", children: code }),
29
- /* @__PURE__ */ jsx("span", { className: `block truncate text-control ${active ? "font-semibold text-slate-900" : "text-slate-800"}`, children: title }),
30
- meta && /* @__PURE__ */ jsx("span", { className: "mt-0.5 flex flex-wrap items-center gap-x-2.5 gap-y-0.5 text-caption text-slate-500", children: meta })
56
+ (code || badges) && /* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 leading-tight", children: [
57
+ code && /* @__PURE__ */ jsx("span", { className: "font-mono text-xs font-semibold text-slate-700", children: realcar(code) }),
58
+ badges
59
+ ] }),
60
+ /* @__PURE__ */ jsx("span", { className: code ? "line-clamp-2 block text-[10px] leading-snug break-words text-slate-600" : "block truncate text-xs text-slate-700", children: realcar(title) }),
61
+ meta && /* @__PURE__ */ jsx("span", { className: "block truncate text-[9px] leading-tight text-slate-400", children: meta })
31
62
  ]
32
63
  }
33
- )
64
+ ),
65
+ aside && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 flex-col items-end gap-1", children: aside })
34
66
  ] });
35
67
  }
36
68
  export {
69
+ HighlightText,
70
+ LINHA_ESBATIDA,
37
71
  ListItem as default
38
72
  };
39
73
  //# sourceMappingURL=ListItem.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ListItem.js","sources":["../../../../../../src/v1/primitives/lists/ListItem/ListItem.tsx"],"sourcesContent":["import React from 'react'\r\nimport Checkbox from '../../forms/Checkbox/Checkbox'\r\n\r\n/** O que se mostra num ListItem: o resto (ativo, marcado) é estado. */\r\nexport interface ListItemContent {\r\n /** A linha principal (ex: a descrição). Numa linha só, cortada com \"…\" se não couber. */\r\n title: React.ReactNode\r\n /** Por cima do título, pequeno e em letra de máquina (ex: o número ou código). */\r\n code?: React.ReactNode\r\n /** A primeira linha: etiquetas e avisos (ex: a categoria). */\r\n badges?: React.ReactNode\r\n /** Por baixo do título: o resto do que se quer ver de relance (local, data, indicadores). */\r\n meta?: React.ReactNode\r\n}\r\n\r\nexport interface ListItemProps extends ListItemContent {\r\n /** O que está aberto ao lado (fica realçado). */\r\n active?: boolean\r\n /** Ao clicar (ou Enter/Espaço com o foco nele): normalmente, abri-lo. */\r\n onClick?: () => void\r\n /** Marcado. Só há caixa de marcar se vier com `onCheckedChange`. */\r\n checked?: boolean\r\n onCheckedChange?: (checked: boolean) => void\r\n /** O nome da caixa de marcar, para leitores de ecrã (ex: \"Marcar P-020000\"). */\r\n checkboxLabel?: string\r\n className?: string\r\n}\r\n\r\n/**\r\n * Uma linha de uma lista para escolher: o cartão das listas do\r\n * [ListFinder](?path=/docs/v1-components-lists-listfinder--docs).\r\n *\r\n * Não sabe o que mostra: a app dá as partes (etiquetas, código, título,\r\n * detalhes) e o ListItem arruma-as sempre da mesma forma. Com\r\n * `onCheckedChange` leva uma caixa à esquerda, para marcar vários (um\r\n * [Checkbox](?path=/docs/v1-primitives-forms-checkbox--docs)).\r\n *\r\n * É um `<li>`: vai dentro de um `<ul>`.\r\n */\r\nexport default function ListItem({\r\n title, code, badges, meta, active = false, onClick, checked = false, onCheckedChange, checkboxLabel, className = '',\r\n}: ListItemProps) {\r\n const fundo = active ? 'bg-proef-blue/8' : checked ? 'bg-slate-50' : 'hover:bg-slate-50'\r\n return (\r\n <li className={`v1-reset relative flex list-none items-start gap-2.5 border-b border-slate-100 px-3 py-2.5 transition-colors ${fundo} ${className}`}>\r\n {/* O que está aberto leva também uma risca à esquerda: com o fundo só,\r\n perdia-se ao lado das linhas marcadas. */}\r\n {active && <span aria-hidden className=\"absolute inset-y-0 left-0 w-0.5 bg-proef-blue\" />}\r\n {onCheckedChange && (\r\n <Checkbox checked={checked} onChange={e => onCheckedChange(e.target.checked)} aria-label={checkboxLabel} className=\"z-10 mt-0.5\" />\r\n )}\r\n {/* O botão cobre a linha toda (o ::after), para se clicar em qualquer\r\n sítio; a caixa de marcar fica por cima dele. */}\r\n <button\r\n type=\"button\"\r\n onClick={onClick}\r\n aria-current={active || undefined}\r\n className=\"min-w-0 flex-1 text-left outline-none after:absolute after:inset-0 focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-primary\"\r\n >\r\n {badges && <span className=\"mb-1 flex flex-wrap items-center gap-1.5\">{badges}</span>}\r\n {code && <span className=\"block font-mono text-micro text-slate-400\">{code}</span>}\r\n <span className={`block truncate text-control ${active ? 'font-semibold text-slate-900' : 'text-slate-800'}`}>{title}</span>\r\n {meta && <span className=\"mt-0.5 flex flex-wrap items-center gap-x-2.5 gap-y-0.5 text-caption text-slate-500\">{meta}</span>}\r\n </button>\r\n </li>\r\n )\r\n}\r\n"],"names":[],"mappings":";;AAuCA,SAAwB,SAAS;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAM,SAAS;AAAA,EAAO;AAAA,EAAS,UAAU;AAAA,EAAO;AAAA,EAAiB;AAAA,EAAe,YAAY;AACnH,GAAkB;AAChB,QAAM,QAAQ,SAAS,oBAAoB,UAAU,gBAAgB;AACrE,8BACG,MAAA,EAAG,WAAW,gHAAgH,KAAK,IAAI,SAAS,IAG9I,UAAA;AAAA,IAAA,UAAU,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,iDAAgD;AAAA,IACtF,mBACC,oBAAC,UAAA,EAAS,SAAkB,UAAU,CAAA,MAAK,gBAAgB,EAAE,OAAO,OAAO,GAAG,cAAY,eAAe,WAAU,eAAc;AAAA,IAInI;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL;AAAA,QACA,gBAAc,UAAU;AAAA,QACxB,WAAU;AAAA,QAET,UAAA;AAAA,UAAA,UAAU,oBAAC,QAAA,EAAK,WAAU,4CAA4C,UAAA,QAAO;AAAA,UAC7E,QAAQ,oBAAC,QAAA,EAAK,WAAU,6CAA6C,UAAA,MAAK;AAAA,UAC3E,oBAAC,UAAK,WAAW,+BAA+B,SAAS,iCAAiC,gBAAgB,IAAK,UAAA,MAAA,CAAM;AAAA,UACpH,QAAQ,oBAAC,QAAA,EAAK,WAAU,sFAAsF,UAAA,KAAA,CAAK;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EACtH,GACF;AAEJ;"}
1
+ {"version":3,"file":"ListItem.js","sources":["../../../../../../src/v1/primitives/lists/ListItem/ListItem.tsx"],"sourcesContent":["import React, { Fragment } from 'react'\r\nimport Checkbox from '../../forms/Checkbox/Checkbox'\r\n\r\n/** O que se mostra num ListItem: o resto (ativo, marcado) é estado. */\r\nexport interface ListItemContent {\r\n /** Por baixo do código: o que a linha é (ex: a descrição). Até duas linhas, cortada com \"…\" se não couber. */\r\n title: React.ReactNode\r\n /** O texto mais forte da linha, em letra de máquina (ex: o número ou código). */\r\n code?: React.ReactNode\r\n /** Na linha do código, logo a seguir a ele: indicadores e avisos curtos. */\r\n badges?: React.ReactNode\r\n /** Por baixo do título, pequeno e numa linha só (cortada com \"…\"): o resto do que se quer ver de relance, separado por \" · \" (ex: local · data). */\r\n meta?: React.ReactNode\r\n /**\r\n * Opcional, e só com caixa de marcar (`onCheckedChange`): uma etiqueta curta\r\n * (ex: a categoria) no espaço que a caixa abre por baixo dela. Sem caixa não\r\n * se mostra — não há coluna para ela.\r\n */\r\n tag?: React.ReactNode\r\n /** À direita, ao nível do código: marcas que se querem ver mesmo a correr a lista. */\r\n aside?: React.ReactNode\r\n}\r\n\r\nexport interface ListItemProps extends ListItemContent {\r\n /** O que está aberto ao lado (fica realçado). */\r\n active?: boolean\r\n /** Ao clicar (ou Enter/Espaço com o foco nele): normalmente, abri-lo. */\r\n onClick?: () => void\r\n /** Marcado. Só há caixa de marcar se vier com `onCheckedChange`. */\r\n checked?: boolean\r\n onCheckedChange?: (checked: boolean) => void\r\n /** O nome da caixa de marcar, para leitores de ecrã (ex: \"Marcar P-020000\"). */\r\n checkboxLabel?: string\r\n /**\r\n * O que se procurou: realça-se no código e no título quando são texto (sem\r\n * acentos nem maiúsculas). O [ListFinder](?path=/docs/v1-components-lists-listfinder--docs) passa-o sozinho.\r\n */\r\n highlight?: string[]\r\n className?: string\r\n}\r\n\r\n/**\r\n * A linha entre linhas, esbatida nas pontas: uma risca de ponta a ponta\r\n * fechava cada linha numa caixa. Desenhada como fundo (e não com um ::after,\r\n * que o botão da linha já usa); a última linha não a leva.\r\n */\r\nexport const LINHA_ESBATIDA = 'bg-[linear-gradient(to_right,transparent,var(--color-slate-200)_15%,var(--color-slate-200)_85%,transparent)] bg-[length:100%_1px] bg-bottom bg-no-repeat last:bg-none'\r\n\r\nconst semAcento = (c: string) => c.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\r\n\r\n/**\r\n * Texto com os pedaços que batem com a pesquisa realçados: numa descrição\r\n * comprida, ou num número de dez dígitos, não se via onde é que o que se\r\n * escreveu bateu.\r\n */\r\nexport function HighlightText({ text, terms }: { text: string; terms?: string[] }) {\r\n const termos = (terms ?? []).map(t => [...t].map(semAcento).join('')).filter(Boolean)\r\n if (!termos.length) return <>{text}</>\r\n // Carácter a carácter, para as posições baterem com o texto original.\r\n const alvo = [...text].map(c => semAcento(c).charAt(0) || c).join('')\r\n const marcado = new Array<boolean>(alvo.length).fill(false)\r\n for (const t of termos) {\r\n for (let i = alvo.indexOf(t); i >= 0; i = alvo.indexOf(t, i + 1)) {\r\n for (let k = i; k < i + t.length; k++) marcado[k] = true\r\n }\r\n }\r\n if (!marcado.some(Boolean)) return <>{text}</>\r\n const chars = [...text]\r\n const partes: { txt: string; hit: boolean }[] = []\r\n chars.forEach((c, i) => {\r\n const ultimo = partes[partes.length - 1]\r\n if (ultimo && ultimo.hit === marcado[i]) ultimo.txt += c\r\n else partes.push({ txt: c, hit: marcado[i] })\r\n })\r\n return (\r\n <>\r\n {partes.map((p, i) => p.hit\r\n ? <mark key={i} className=\"rounded-sm bg-amber-200 font-bold text-amber-900\">{p.txt}</mark>\r\n : <Fragment key={i}>{p.txt}</Fragment>)}\r\n </>\r\n )\r\n}\r\n\r\n/**\r\n * Uma linha de uma lista para escolher: o cartão das listas do\r\n * [ListFinder](?path=/docs/v1-components-lists-listfinder--docs).\r\n *\r\n * Não sabe o que mostra: a app dá as partes e o ListItem arruma-as sempre da\r\n * mesma forma. O código é o texto mais forte da linha, com os indicadores ao\r\n * lado; o título vem por baixo, mais pequeno, e o resto por baixo dele. Com\r\n * `onCheckedChange` leva uma caixa à esquerda, para marcar vários (um\r\n * [Checkbox](?path=/docs/v1-primitives-forms-checkbox--docs)), numa coluna de\r\n * largura fixa — senão os títulos começavam em sítios diferentes de linha para\r\n * linha — e, se se quiser, a `tag` por baixo dela.\r\n *\r\n * É um `<li>`: vai dentro de um `<ul>`.\r\n */\r\nexport default function ListItem({\r\n title, code, badges, meta, tag, aside, active = false, onClick, checked = false, onCheckedChange, checkboxLabel,\r\n highlight, className = '',\r\n}: ListItemProps) {\r\n const fundo = active ? 'bg-blue-50' : checked ? 'bg-proef-turquoise/5' : 'hover:bg-slate-50'\r\n const realcar = (n: React.ReactNode) => (typeof n === 'string' ? <HighlightText text={n} terms={highlight} /> : n)\r\n return (\r\n <li className={`v1-reset relative flex list-none items-start gap-1.5 px-3 py-3 transition-colors ${LINHA_ESBATIDA} ${fundo} ${className}`}>\r\n {onCheckedChange && (\r\n <span className=\"flex w-7 shrink-0 flex-col items-center gap-1\">\r\n <Checkbox checked={checked} onChange={e => onCheckedChange(e.target.checked)} aria-label={checkboxLabel} className=\"z-10 mt-0.5\" />\r\n {tag && <span className=\"text-[8px] leading-none font-semibold text-slate-400\">{tag}</span>}\r\n </span>\r\n )}\r\n {/* O botão cobre a linha toda (o ::after), para se clicar em qualquer\r\n sítio; a caixa de marcar fica por cima dele. */}\r\n <button\r\n type=\"button\"\r\n onClick={onClick}\r\n aria-current={active || undefined}\r\n className=\"min-w-0 flex-1 space-y-1 text-left outline-none after:absolute after:inset-0 focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-primary\"\r\n >\r\n {(code || badges) && (\r\n <span className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5 leading-tight\">\r\n {code && <span className=\"font-mono text-xs font-semibold text-slate-700\">{realcar(code)}</span>}\r\n {badges}\r\n </span>\r\n )}\r\n {/* Sem código, o título é a linha principal. */}\r\n <span className={code\r\n ? 'line-clamp-2 block text-[10px] leading-snug break-words text-slate-600'\r\n : 'block truncate text-xs text-slate-700'}>\r\n {realcar(title)}\r\n </span>\r\n {meta && <span className=\"block truncate text-[9px] leading-tight text-slate-400\">{meta}</span>}\r\n </button>\r\n {aside && <span className=\"flex shrink-0 flex-col items-end gap-1\">{aside}</span>}\r\n </li>\r\n )\r\n}\r\n"],"names":["Fragment"],"mappings":"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"}