ArgumentForm.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  1. <template>
  2. <section id = "argument-popup">
  3. <div class="section-header">
  4. <h2>Start action: {{ title }}</h2>
  5. </div>
  6. <div class="section-content padding">
  7. <form @submit="handleSubmit">
  8. <template v-if="actionArguments.length > 0">
  9. <template v-for="arg in actionArguments" :key="arg.name">
  10. <label :for="arg.name">
  11. {{ formatLabel(arg.title) }}
  12. </label>
  13. <template v-if="arg.type === 'file_upload'">
  14. <div class="file-upload-field">
  15. <div
  16. class="file-upload-dropzone"
  17. :class="{ 'file-upload-dropzone--active': (fileUploadDragDepth[arg.name] || 0) > 0 }"
  18. @dragenter.prevent="onFileDragEnter(arg)"
  19. @dragover.prevent="onFileDragOver"
  20. @dragleave="onFileDragLeave(arg)"
  21. @drop.prevent="onFileDrop(arg, $event)"
  22. >
  23. <input
  24. :id="arg.name"
  25. :name="arg.name"
  26. type="file"
  27. class="file-upload-input-overlay"
  28. :accept="getFileAccept(arg)"
  29. @change="handleChange(arg, $event)"
  30. />
  31. <div class="file-upload-dropzone-inner">
  32. <span class="file-upload-prompt">{{ fileUploadPrompt(arg) }}</span>
  33. <span v-if="formErrors[arg.name]" class="file-upload-error">{{ formErrors[arg.name] }}</span>
  34. </div>
  35. </div>
  36. </div>
  37. <span class="argument-description">
  38. <p v-html="arg.description"></p>
  39. <p v-if="maxUploadSizeSummary(arg)" class="file-upload-mime-types">{{ maxUploadSizeSummary(arg) }}</p>
  40. <p v-if="mimeTypesSummary(arg)" class="file-upload-mime-types">{{ mimeTypesSummary(arg) }}</p>
  41. </span>
  42. </template>
  43. <template v-else>
  44. <datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="`${arg.name}-choices`">
  45. <option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
  46. {{ suggestion }}
  47. </option>
  48. <option v-for="(suggestion, index) in getBrowserSuggestions(arg)" :key="`browser-${index}`" :value="suggestion">
  49. {{ suggestion }}
  50. </option>
  51. </datalist>
  52. <select v-if="getInputComponent(arg) === 'select'" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
  53. :required="arg.required" @input="handleInput(arg, $event)" @change="handleChange(arg, $event)">
  54. <option v-for="choice in arg.choices" :key="choice.value" :value="choice.value">
  55. {{ choice.title || choice.value }}
  56. </option>
  57. </select>
  58. <component v-else :is="getInputComponent(arg)" :id="arg.name" :name="arg.name"
  59. :value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
  60. :checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
  61. :list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? `${arg.name}-choices` : undefined"
  62. :type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
  63. :rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
  64. :step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
  65. @input="handleInput(arg, $event)" @change="handleChange(arg, $event)" />
  66. <span class="argument-description" v-html="arg.description"></span>
  67. </template>
  68. </template>
  69. </template>
  70. <div v-else>
  71. <p>No arguments required</p>
  72. </div>
  73. <div class="buttons">
  74. <button name="start" type="submit" :disabled="hasConfirmation && !confirmationChecked">
  75. Start
  76. </button>
  77. <button name="cancel" type="button" @click="handleCancel">
  78. Cancel
  79. </button>
  80. </div>
  81. </form>
  82. </div>
  83. </section>
  84. </template>
  85. <script setup>
  86. import { ref, reactive, onMounted, nextTick } from 'vue'
  87. import { useRouter } from 'vue-router'
  88. const router = useRouter()
  89. // Reactive data
  90. const dialog = ref(null)
  91. const title = ref('')
  92. const icon = ref('')
  93. //const arguments = ref([])
  94. const argValues = ref({})
  95. const confirmationChecked = ref(false)
  96. const hasConfirmation = ref(false)
  97. const formErrors = ref({})
  98. const actionArguments = ref([])
  99. const popupOnStart = ref('')
  100. const fileUploadDragDepth = reactive({})
  101. const fileUploadDisplayName = reactive({})
  102. // Computed properties
  103. const props = defineProps({
  104. bindingId: {
  105. type: String,
  106. required: true
  107. }
  108. })
  109. // Methods
  110. async function setup() {
  111. const ret = await window.client.getActionBinding({
  112. bindingId: props.bindingId
  113. })
  114. const action = ret.action
  115. title.value = action.title
  116. icon.value = action.icon
  117. popupOnStart.value = action.popupOnStart || ''
  118. actionArguments.value = action.arguments || []
  119. argValues.value = {}
  120. formErrors.value = {}
  121. for (const key of Object.keys(fileUploadDragDepth)) {
  122. delete fileUploadDragDepth[key]
  123. }
  124. for (const key of Object.keys(fileUploadDisplayName)) {
  125. delete fileUploadDisplayName[key]
  126. }
  127. confirmationChecked.value = false
  128. hasConfirmation.value = false
  129. // Initialize values from query params or defaults
  130. actionArguments.value.forEach(arg => {
  131. if (arg.type === 'confirmation') {
  132. hasConfirmation.value = true
  133. const paramValue = getQueryParamValue(arg.name)
  134. let checkedValue = false
  135. if (paramValue !== null) {
  136. checkedValue = paramValue === '1' || paramValue === 'true' || paramValue === true
  137. } else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
  138. checkedValue = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
  139. }
  140. argValues.value[arg.name] = checkedValue
  141. confirmationChecked.value = checkedValue
  142. } else {
  143. const paramValue = getQueryParamValue(arg.name)
  144. if (arg.type === 'checkbox') {
  145. // For checkboxes, handle boolean default values properly
  146. if (paramValue !== null) {
  147. argValues.value[arg.name] = paramValue === '1' || paramValue === 'true' || paramValue === true
  148. } else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
  149. argValues.value[arg.name] = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
  150. } else {
  151. argValues.value[arg.name] = false
  152. }
  153. } else {
  154. argValues.value[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
  155. }
  156. }
  157. })
  158. // Run initial validation on all fields after DOM is updated
  159. await nextTick()
  160. for (const arg of actionArguments.value) {
  161. if (arg.type && !arg.type.startsWith('regex:') && arg.type !== 'select' && arg.type !== '' && arg.type !== 'confirmation' && arg.type !== 'checkbox' && arg.type !== 'file_upload') {
  162. await validateArgument(arg, argValues.value[arg.name] || '')
  163. }
  164. }
  165. }
  166. function getQueryParamValue(paramName) {
  167. const params = new URLSearchParams(window.location.search.substring(1))
  168. return params.get(paramName)
  169. }
  170. function formatLabel(title) {
  171. const lastChar = title.charAt(title.length - 1)
  172. if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
  173. return title
  174. }
  175. return title + ':'
  176. }
  177. function getInputComponent(arg) {
  178. if (arg.type === 'html') {
  179. return 'div'
  180. } else if (arg.type === 'raw_string_multiline') {
  181. return 'textarea'
  182. } else if (arg.choices && arg.choices.length > 0 && (arg.type === 'select' || arg.type === '')) {
  183. return 'select'
  184. } else {
  185. return 'input'
  186. }
  187. }
  188. function getFileAccept(arg) {
  189. if (arg.type !== 'file_upload' || !arg.allowedMimeTypes || arg.allowedMimeTypes.length === 0) {
  190. return undefined
  191. }
  192. return arg.allowedMimeTypes.join(',')
  193. }
  194. function mimeTypesSummary(arg) {
  195. if (arg.type !== 'file_upload' || !arg.allowedMimeTypes || arg.allowedMimeTypes.length === 0) {
  196. return ''
  197. }
  198. return 'Supported MIME types: ' + arg.allowedMimeTypes.join(', ')
  199. }
  200. /** SI byte formatting (matches server-side humanize-style defaults such as "10 MB"). */
  201. function formatBytesDecimal(numBytes) {
  202. if (!Number.isFinite(numBytes) || numBytes < 0) {
  203. return ''
  204. }
  205. const n = Math.floor(numBytes)
  206. if (n < 1000) {
  207. return `${n} B`
  208. }
  209. const units = ['kB', 'MB', 'GB', 'TB']
  210. let v = n
  211. let i = 0
  212. while (v >= 1000 && i < units.length) {
  213. v /= 1000
  214. i++
  215. }
  216. const unit = units[i - 1]
  217. const rounded = v < 10 ? Math.round(v * 10) / 10 : Math.round(v)
  218. return `${rounded} ${unit}`
  219. }
  220. function maxUploadSizeSummary(arg) {
  221. if (arg.type !== 'file_upload') {
  222. return ''
  223. }
  224. const max = maxUploadBytesNumber(arg)
  225. if (max <= 0) {
  226. return ''
  227. }
  228. return `Max file size: ${formatBytesDecimal(max)}`
  229. }
  230. function fileUploadPrompt(arg) {
  231. if (fileUploadDisplayName[arg.name]) {
  232. return fileUploadDisplayName[arg.name]
  233. }
  234. return 'Drop a file here or click to browse'
  235. }
  236. function onFileDragEnter(arg) {
  237. fileUploadDragDepth[arg.name] = (fileUploadDragDepth[arg.name] || 0) + 1
  238. }
  239. function onFileDragLeave(arg) {
  240. const next = Math.max(0, (fileUploadDragDepth[arg.name] || 0) - 1)
  241. if (next === 0) {
  242. delete fileUploadDragDepth[arg.name]
  243. } else {
  244. fileUploadDragDepth[arg.name] = next
  245. }
  246. }
  247. function onFileDragOver(event) {
  248. event.dataTransfer.dropEffect = 'copy'
  249. }
  250. function onFileDrop(arg, event) {
  251. delete fileUploadDragDepth[arg.name]
  252. const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]
  253. if (file) {
  254. processStagedFileUpload(arg, file)
  255. }
  256. }
  257. function maxUploadBytesNumber(arg) {
  258. if (arg.maxUploadBytes === undefined || arg.maxUploadBytes === null) {
  259. return 0
  260. }
  261. return typeof arg.maxUploadBytes === 'bigint' ? Number(arg.maxUploadBytes) : Number(arg.maxUploadBytes)
  262. }
  263. function getInputType(arg) {
  264. if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
  265. return undefined
  266. }
  267. if (arg.type === 'confirmation') {
  268. return 'checkbox'
  269. }
  270. if (arg.type === 'file_upload') {
  271. return 'file'
  272. }
  273. if (arg.type === 'ascii_identifier' || arg.type === 'ascii') {
  274. return 'text'
  275. }
  276. if (arg.type === 'datetime') {
  277. return 'datetime-local'
  278. }
  279. return arg.type
  280. }
  281. function getPattern(arg) {
  282. if (arg.type && arg.type.startsWith('regex:')) {
  283. return arg.type.replace('regex:', '')
  284. }
  285. return undefined
  286. }
  287. function getArgumentValue(arg) {
  288. if (arg.type === 'checkbox' || arg.type === 'confirmation') {
  289. return argValues.value[arg.name] === '1' || argValues.value[arg.name] === true || argValues.value[arg.name] === 'true'
  290. }
  291. return argValues.value[arg.name] || ''
  292. }
  293. function handleInput(arg, event) {
  294. const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
  295. argValues.value[arg.name] = value
  296. updateUrlWithArg(arg.name, value)
  297. }
  298. async function uploadStagedFile(arg, file) {
  299. const formData = new FormData()
  300. formData.append('binding_id', props.bindingId)
  301. formData.append('argument_name', arg.name)
  302. formData.append('file', file)
  303. const res = await fetch('/api/upload/action-argument', {
  304. method: 'POST',
  305. body: formData,
  306. credentials: 'same-origin'
  307. })
  308. const text = await res.text()
  309. if (!res.ok) {
  310. throw new Error(text || `Upload failed (${res.status})`)
  311. }
  312. let data
  313. try {
  314. data = JSON.parse(text)
  315. } catch (e) {
  316. throw new Error('Invalid upload response')
  317. }
  318. if (!data.uploadToken) {
  319. throw new Error('Upload response missing token')
  320. }
  321. return data.uploadToken
  322. }
  323. function handleChange(arg, event) {
  324. if (arg.type === 'confirmation') {
  325. confirmationChecked.value = event.target.checked
  326. return
  327. }
  328. if (arg.type === 'file_upload') {
  329. handleFileUploadChange(arg, event)
  330. return
  331. }
  332. // Validate the input
  333. validateArgument(arg, event.target.value)
  334. }
  335. async function processStagedFileUpload(arg, file) {
  336. const inputEl = document.getElementById(arg.name)
  337. if (!file) {
  338. argValues.value[arg.name] = ''
  339. delete fileUploadDisplayName[arg.name]
  340. if (inputEl) {
  341. inputEl.setCustomValidity('')
  342. }
  343. return
  344. }
  345. const maxBytes = maxUploadBytesNumber(arg)
  346. if (maxBytes > 0 && file.size > maxBytes) {
  347. const msg = `File is too large (max ${formatBytesDecimal(maxBytes)})`
  348. if (inputEl) {
  349. inputEl.setCustomValidity(msg)
  350. }
  351. formErrors.value[arg.name] = msg
  352. delete fileUploadDisplayName[arg.name]
  353. return
  354. }
  355. try {
  356. const token = await uploadStagedFile(arg, file)
  357. argValues.value[arg.name] = token
  358. fileUploadDisplayName[arg.name] = file.name
  359. if (inputEl) {
  360. inputEl.setCustomValidity('')
  361. }
  362. delete formErrors.value[arg.name]
  363. await validateArgument(arg, token)
  364. } catch (err) {
  365. console.warn('Upload failed:', err)
  366. const msg = err.message || 'Upload failed'
  367. formErrors.value[arg.name] = msg
  368. if (inputEl) {
  369. inputEl.setCustomValidity(msg)
  370. }
  371. argValues.value[arg.name] = ''
  372. delete fileUploadDisplayName[arg.name]
  373. }
  374. }
  375. async function handleFileUploadChange(arg, event) {
  376. const file = event.target.files && event.target.files[0]
  377. await processStagedFileUpload(arg, file)
  378. }
  379. async function validateArgument(arg, value) {
  380. if (!arg.type || arg.type.startsWith('regex:')) {
  381. return
  382. }
  383. // Skip validation for datetime - backend will handle mangling values without seconds
  384. if (arg.type === 'datetime') {
  385. const inputElement = document.getElementById(arg.name)
  386. if (inputElement) {
  387. inputElement.setCustomValidity('')
  388. }
  389. delete formErrors.value[arg.name]
  390. return
  391. }
  392. // Skip validation for checkbox and confirmation - they're always valid
  393. if (arg.type === 'checkbox' || arg.type === 'confirmation') {
  394. const inputElement = document.getElementById(arg.name)
  395. if (inputElement) {
  396. inputElement.setCustomValidity('')
  397. }
  398. delete formErrors.value[arg.name]
  399. return
  400. }
  401. try {
  402. const validateArgumentTypeArgs = {
  403. value: value,
  404. type: arg.type,
  405. bindingId: props.bindingId,
  406. argumentName: arg.name
  407. }
  408. const validation = await window.client.validateArgumentType(validateArgumentTypeArgs)
  409. // Get the input element to set custom validity
  410. const inputElement = document.getElementById(arg.name)
  411. if (validation.valid) {
  412. delete formErrors.value[arg.name]
  413. // Clear custom validity message
  414. if (inputElement) {
  415. inputElement.setCustomValidity('')
  416. }
  417. } else {
  418. formErrors.value[arg.name] = validation.description
  419. // Set custom validity message
  420. if (inputElement) {
  421. inputElement.setCustomValidity(validation.description)
  422. }
  423. }
  424. } catch (err) {
  425. console.warn('Validation failed:', err)
  426. const inputElement = document.getElementById(arg.name)
  427. if (arg.type === 'file_upload') {
  428. const msg = 'Could not validate upload; try again or check your connection'
  429. formErrors.value[arg.name] = msg
  430. if (inputElement) {
  431. inputElement.setCustomValidity(msg)
  432. }
  433. } else if (inputElement) {
  434. inputElement.setCustomValidity('')
  435. }
  436. }
  437. }
  438. function updateUrlWithArg(name, value) {
  439. if (name && value !== undefined) {
  440. const url = new URL(window.location.href)
  441. // Don't add passwords to URL
  442. const arg = actionArguments.value.find(a => a.name === name)
  443. if (arg && (arg.type === 'password' || arg.type === 'file_upload')) {
  444. return
  445. }
  446. url.searchParams.set(name, value)
  447. window.history.replaceState({}, '', url.toString())
  448. }
  449. }
  450. function getArgumentValues() {
  451. const ret = []
  452. for (const arg of actionArguments.value) {
  453. let value = argValues.value[arg.name] || ''
  454. if (arg.type === 'checkbox' || arg.type === 'confirmation') {
  455. value = value ? '1' : '0'
  456. }
  457. ret.push({
  458. name: arg.name,
  459. value: value
  460. })
  461. }
  462. return ret
  463. }
  464. function getUniqueId() {
  465. if (window.isSecureContext) {
  466. return window.crypto.randomUUID()
  467. } else {
  468. return Date.now().toString()
  469. }
  470. }
  471. function getBrowserSuggestions(arg) {
  472. if (!arg.suggestionsBrowserKey) {
  473. return []
  474. }
  475. try {
  476. const stored = localStorage.getItem(`olivetin-suggestions-${arg.suggestionsBrowserKey}`)
  477. if (stored) {
  478. const suggestions = JSON.parse(stored)
  479. return Array.isArray(suggestions) ? suggestions : []
  480. }
  481. } catch (err) {
  482. console.warn('Failed to load browser suggestions:', err)
  483. }
  484. return []
  485. }
  486. function saveBrowserSuggestions() {
  487. for (const arg of actionArguments.value) {
  488. if (arg.suggestionsBrowserKey) {
  489. const value = argValues.value[arg.name]
  490. // Only save non-empty values for non-checkbox/confirmation/password types
  491. if (value && value !== '' && arg.type !== 'checkbox' && arg.type !== 'confirmation' && arg.type !== 'password' && arg.type !== 'file_upload') {
  492. try {
  493. const key = `olivetin-suggestions-${arg.suggestionsBrowserKey}`
  494. const stored = localStorage.getItem(key)
  495. let suggestions = []
  496. if (stored) {
  497. suggestions = JSON.parse(stored)
  498. if (!Array.isArray(suggestions)) {
  499. suggestions = []
  500. }
  501. }
  502. // Add value if not already present
  503. if (!suggestions.includes(value)) {
  504. suggestions.unshift(value) // Add to beginning
  505. // Keep only the most recent 50 suggestions
  506. if (suggestions.length > 50) {
  507. suggestions = suggestions.slice(0, 50)
  508. }
  509. localStorage.setItem(key, JSON.stringify(suggestions))
  510. }
  511. } catch (err) {
  512. console.warn('Failed to save browser suggestions:', err)
  513. }
  514. }
  515. }
  516. }
  517. }
  518. async function startAction(actionArgs) {
  519. const startActionArgs = {
  520. bindingId: props.bindingId,
  521. arguments: actionArgs,
  522. uniqueTrackingId: getUniqueId()
  523. }
  524. try {
  525. const response = await window.client.startAction(startActionArgs)
  526. console.log('Action started successfully with tracking ID:', response.executionTrackingId)
  527. return response
  528. } catch (err) {
  529. console.error('Failed to start action:', err)
  530. throw err
  531. }
  532. }
  533. async function handleSubmit(event) {
  534. // Set custom validity for required fields
  535. for (const arg of actionArguments.value) {
  536. const value = argValues.value[arg.name]
  537. const inputElement = document.getElementById(arg.name)
  538. if (arg.required && (!value || value === '')) {
  539. formErrors.value[arg.name] = 'This field is required'
  540. // Set custom validity for required field validation
  541. if (inputElement) {
  542. inputElement.setCustomValidity('This field is required')
  543. }
  544. }
  545. }
  546. const form = event.target
  547. if (!form.checkValidity()) {
  548. console.log('argument form has elements that failed validation')
  549. return
  550. }
  551. event.preventDefault()
  552. const argvs = getArgumentValues()
  553. console.log('argument form has elements that passed validation')
  554. // Save values to localStorage for arguments with suggestionsBrowserKey
  555. saveBrowserSuggestions()
  556. try {
  557. const response = await startAction(argvs)
  558. if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
  559. router.push(`/logs/${response.executionTrackingId}`)
  560. } else {
  561. router.back()
  562. }
  563. } catch (err) {
  564. console.error('Failed to start action:', err)
  565. }
  566. }
  567. function handleCancel() {
  568. router.back()
  569. clearBookmark()
  570. }
  571. function clearBookmark() {
  572. window.history.replaceState({
  573. path: window.location.pathname
  574. }, '', window.location.pathname)
  575. }
  576. function show() {
  577. if (dialog.value) {
  578. dialog.value.showModal()
  579. }
  580. }
  581. function close() {
  582. if (dialog.value) {
  583. dialog.value.close()
  584. }
  585. }
  586. // Expose methods for parent components
  587. defineExpose({
  588. show,
  589. close
  590. })
  591. // Lifecycle
  592. onMounted(() => {
  593. setup()
  594. })
  595. </script>
  596. <style scoped>
  597. form {
  598. grid-template-columns: max-content auto auto;
  599. }
  600. .file-upload-field {
  601. display: flex;
  602. flex-direction: column;
  603. gap: 0.5rem;
  604. min-width: 0;
  605. }
  606. .file-upload-dropzone {
  607. position: relative;
  608. min-height: 5.5rem;
  609. border: 2px dashed #bbb;
  610. border-radius: 0.5rem;
  611. background: #fafafa;
  612. transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
  613. }
  614. .file-upload-dropzone:hover:not(.file-upload-dropzone--active) {
  615. border-color: #7a9bbb;
  616. background: #f3f7fb;
  617. box-shadow: 0 2px 8px rgba(68, 136, 204, 0.12);
  618. }
  619. .file-upload-dropzone--active {
  620. border-color: #4488cc;
  621. background: #f0f6fc;
  622. }
  623. @media (prefers-color-scheme: dark) {
  624. .file-upload-dropzone {
  625. border-color: #555;
  626. background: #222;
  627. }
  628. .file-upload-dropzone:hover:not(.file-upload-dropzone--active) {
  629. border-color: #7a9bbb;
  630. background: #222;
  631. box-shadow: 0 2px 8px rgba(68, 136, 204, 0.12);
  632. }
  633. .file-upload-dropzone--active {
  634. border-color: #4488cc;
  635. background: #2a3b4c;
  636. }
  637. }
  638. .file-upload-input-overlay {
  639. position: absolute;
  640. inset: 0;
  641. width: 100%;
  642. height: 100%;
  643. margin: 0;
  644. padding: 0;
  645. opacity: 0;
  646. cursor: pointer;
  647. z-index: 2;
  648. font-size: 0;
  649. }
  650. .file-upload-dropzone-inner {
  651. position: relative;
  652. z-index: 1;
  653. display: flex;
  654. flex-direction: column;
  655. align-items: center;
  656. justify-content: center;
  657. gap: 0.35rem;
  658. min-height: 5.5rem;
  659. padding: 0.75rem 1rem;
  660. text-align: center;
  661. pointer-events: none;
  662. }
  663. .file-upload-prompt {
  664. font-size: 0.9375rem;
  665. word-break: break-word;
  666. }
  667. .file-upload-error {
  668. font-size: 0.8125rem;
  669. color: #b00020;
  670. }
  671. .file-upload-mime-types {
  672. font-size: 0.8125rem;
  673. color: #555;
  674. margin: 0;
  675. margin-top: 0.15rem;
  676. }
  677. .argument-description {
  678. font-size: 0.875rem;
  679. color: #666;
  680. margin-top: 0.25rem;
  681. }
  682. .buttons {
  683. display: flex;
  684. gap: 0.5rem;
  685. justify-content: flex-end;
  686. padding-top: 1rem;
  687. border-top: 1px solid #eee;
  688. }
  689. /* Checkbox specific styling */
  690. .argument-group input[type="checkbox"] {
  691. width: auto;
  692. margin-right: 0.5rem;
  693. }
  694. .argument-group input[type="checkbox"]+label {
  695. display: inline;
  696. font-weight: normal;
  697. }
  698. </style>