diff --git a/.github/hero-image.png b/.github/hero-image.png index 18b6b12..39b98d3 100644 Binary files a/.github/hero-image.png and b/.github/hero-image.png differ diff --git a/README.md b/README.md index 0bc5ff6..5195f34 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Each object can have the following properties: - `thick_line` (function) - takes in `currentDate`, returns Boolean determining whether the line for that date should be thicker than the others. Three other options allow you to override general configuration for this view mode alone: -- `format_string` +- `date_format` - `column_width` - `snap_at` For details, see the above table. diff --git a/builder/demo.css b/builder/demo.css index 09b81ab..89f79b9 100644 --- a/builder/demo.css +++ b/builder/demo.css @@ -1,63 +1,64 @@ .switch { - position: relative; - display: inline-block; - width: 50px; - height: 20px; - float: right; + position: relative; + display: inline-block; + width: 50px; + height: 20px; + float: right; } .switch input { - opacity: 0; - width: 0; - height: 0; + opacity: 0; + width: 0; + height: 0; } .slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ddd; - -webkit-transition: .2s; - transition: .2s; - border: 1px solid #aaa; + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ddd; + -webkit-transition: 0.2s; + transition: 0.2s; + border: 1px solid #37352f; + scale: 0.75; } .slider:before { - position: absolute; - content: ""; - height: 12px; - width: 12px; - left: 4px; - bottom: 3px; - background-color: white; - -webkit-transition: .2s; - transition: .2s; + position: absolute; + content: ''; + height: 12px; + width: 12px; + left: 4px; + bottom: 3px; + background-color: white; + -webkit-transition: 0.2s; + transition: 0.2s; } input:checked + .slider { - background-color: #0d6efd; - border-color: #0d6efd; + background-color: #7c7c7c; + border-color: #7c7c7c; } input:focus + .slider { - box-shadow: none; + box-shadow: none; } input:checked + .slider:before { - -webkit-transform: translateX(28px); - -ms-transform: translateX(28px); - transform: translateX(28px); + -webkit-transform: translateX(28px); + -ms-transform: translateX(28px); + transform: translateX(28px); } .slider.round { - border-radius: 25px; + border-radius: 25px; } .slider.round:before { - border-radius: 50%; + border-radius: 50%; } .viewmode-select { @@ -65,7 +66,7 @@ input:checked + .slider:before { } .selected { - border: 1.5px solid black !important; + border: 1.5px solid black !important; } .button { @@ -84,22 +85,31 @@ input:checked + .slider:before { } .input-switch { - width: fit-content; + align-items: center; + width: 45%; + display: flex; + justify-content: space-between; } .input-switch label { - padding-right: 50px; + padding-right: 30px; font-size: 14px; } .code { display: block; - background: none; + background: 0; white-space: pre; - -webkit-overflow-scrolling: touch; overflow-x: scroll; max-width: 100%; min-width: 100px; padding: 0; -} + font-family: monospace; + padding-top: 0.8571429em; + padding-right: 1.1428571em; + padding-bottom: 0.8571429em; + padding-left: 1.1428571em; + background: #1f2937; + color: #e5e7eb; + border-radius: 3px; } diff --git a/builder/demo.js b/builder/demo.js index ba20bea..a9f9c5f 100644 --- a/builder/demo.js +++ b/builder/demo.js @@ -1,81 +1,4 @@ -let today = new Date(); -today.setHours(0, 0, 0, 0); -today = today.valueOf(); - -function random(begin = 10, end = 90, multiple = 10) { - let k; - do { - k = Math.floor(Math.random() * 100); - } while (k < begin || k > end || k % multiple !== 0); - return k; -} -const daysSince = (dx) => new Date(today + dx * 86400000); const tasks = [ - { - start: daysSince(-2), - end: daysSince(2), - name: 'Redesign website', - id: 'Task 0', - progress: random(), - }, - { - start: daysSince(3), - duration: '6d', - name: 'Write new content', - id: 'Task 1', - progress: random(), - important: true, - dependencies: 'Task 0', - }, - { - start: daysSince(4), - duration: '2d', - name: 'Apply new styles', - id: 'Task 2', - progress: random(), - }, - { - start: daysSince(-4), - end: daysSince(0), - name: 'Review', - id: 'Task 3', - progress: random(), - }, -]; - -const tasksSpread = [ - { - start: daysSince(-30), - end: daysSince(-10), - name: 'Redesign website', - id: 'Task 0', - progress: random(), - }, - { - start: daysSince(-15), - duration: '21d', - name: 'Write new content', - id: 'Task 1', - progress: random(), - important: true, - }, - { - start: daysSince(10), - duration: '14d', - name: 'Review', - id: 'Task 3', - progress: random(), - }, - { - start: daysSince(-3), - duration: '4d', - name: 'Publish', - id: 'Task 4', - progress: random(), - }, -]; - -let tasksMany = [ { start: daysSince(-7), end: daysSince(-5), @@ -128,6 +51,7 @@ let tasksMany = [ duration: '3d', name: 'Prepare demo', id: 'Task 6', + dependencies: 'Task 5', progress: random(), }, { @@ -135,7 +59,7 @@ let tasksMany = [ end: daysSince(12), name: 'Final client review', id: 'Task 7', - progress: random(), + progress: 0, important: true, }, { @@ -143,107 +67,171 @@ let tasksMany = [ duration: '6d', name: 'Implement feedback', id: 'Task 8', + progress: 0, + }, +]; + +const tasksSmall = [ + { + start: daysSince(-2), + end: daysSince(2), + name: 'Redesign website', + id: 'Task 0', progress: random(), }, { - start: daysSince(16), - duration: '4d', - name: 'Launch website', - id: 'Task 9', + start: daysSince(3), + duration: '6d', + name: 'Write new content', + id: 'Task 1', progress: random(), important: true, + dependencies: 'Task 0', + }, + { + start: daysSince(4), + duration: '2d', + name: 'Apply new styles', + id: 'Task 2', + progress: random(), + }, + { + start: daysSince(-4), + end: daysSince(0), + name: 'Review', + id: 'Task 3', + progress: random(), + }, +]; + +const tasksBlank = [ + { + start: daysSince(1), + duration: '3d', + name: 'Marketing Strategy Review', + id: 'Task 1', + important: true, + }, + { + start: daysSince(-2), + end: daysSince(12), + name: 'Mentor Sooriya', + id: 'Task 0', + }, + { + start: daysSince(4), + end: daysSince(5), + name: 'Investors Meetup', + id: 'Task 3', }, ]; const HOLIDAYS = [ - { name: 'Republic Day', date: '2024-01-26' }, - { name: 'Maha Shivratri', date: '2024-02-23' }, - { name: 'Holi', date: '2024-03-11' }, - { name: 'Mahavir Jayanthi', date: '2024-04-07' }, - { name: 'Good Friday', date: '2024-04-10' }, - { name: 'May Day', date: '2024-05-01' }, - { name: 'Buddha Purnima', date: '2024-05-08' }, - { name: 'Krishna Janmastami', date: '2024-08-14' }, - { name: 'Independence Day', date: '2024-08-15' }, - { name: 'Ganesh Chaturthi', date: '2024-08-23' }, - { name: 'Id-Ul-Fitr', date: '2024-09-21' }, - { name: 'Vijaya Dashami', date: '2024-09-28' }, - { name: 'Mahatma Gandhi Jayanti', date: '2024-10-02' }, - { name: 'Diwali', date: '2024-10-17' }, - { name: 'Guru Nanak Jayanthi', date: '2024-11-02' }, - { name: 'Christmas', date: '2024-12-25' }, + { name: 'New Years Day', date: '2025-01-01' }, + { name: 'Republic Day', date: '2025-01-26' }, + { name: 'Maha Shivratri', date: '2025-02-23' }, + { name: 'Holi', date: '2025-03-11' }, + { name: 'Mahavir Jayanthi', date: '2025-04-07' }, + { name: 'Good Friday', date: '2025-04-10' }, + { name: 'May Day', date: '2025-05-01' }, + { name: 'Buddha Purnima', date: '2025-05-08' }, + { name: 'Krishna Janmastami', date: '2025-08-14' }, + { name: 'Independence Day', date: '2025-08-15' }, + { name: 'Ganesh Chaturthi', date: '2025-08-23' }, + { name: 'Id-Ul-Fitr', date: '2025-09-21' }, + { name: 'Vijaya Dashami', date: '2025-09-28' }, + { name: 'Mahatma Gandhi Jayanti', date: '2025-10-02' }, + { name: 'Diwali', date: '2025-10-17' }, + { name: 'Guru Nanak Jayanthi', date: '2025-11-02' }, + { name: 'Christmas', date: '2025-12-25' }, ]; -const mutablity = new Gantt('#mutability', tasks, { - holidays: null, +new Gantt('#central-demo', tasks, { scroll_to: daysSince(-7), infinite_padding: false, }); -const sideheader = new Gantt('#sideheader', tasks, { - holidays: null, +const sideheader = new Gantt('#sideheader', tasksSmall, { + scroll_to: daysSince(-20), view_mode_select: true, - scroll_to: null, infinite_padding: false, }); -const holidays = new Gantt('#holidays', tasksSpread, { +const popup = new Gantt('#popup', tasksBlank, { + scroll_to: daysSince(-7), + infinite_padding: false, + container_height: 350, + popup: (ctx) => { + ctx.set_title(ctx.task.name); + let title = ctx.get_title(); + title.style.border = '0.5px solid black'; + title.style.borderRadius = '1.5px'; + title.style.padding = '3px 5px '; + title.style.backgroundColor = 'black'; + title.style.opacity = '0.85'; + title.style.color = 'white'; + title.style.width = 'fit-content'; + title.onclick = () => { + let ans = prompt('New Title: '); + if (ans) ctx.set_title(ans); + }; + if (ctx.task.description) ctx.set_subtitle(ctx.task.description); + else ctx.set_subtitle(''); + + ctx.set_details( + `Duration: ${ctx.task.actual_duration} days
Dates: ${ctx.task._start.toLocaleDateString('en-US')} - ${ctx.task._end.toLocaleDateString('en-US')}`, + ); + let details = ctx.get_details(); + details.style.lineHeight = '1.75'; + details.style.margin = '10px 4px'; + if (!ctx.chart.options.readonly) { + if (!ctx.chart.options.readonly_progress) { + ctx.add_action('Set Color', (task, chart) => { + const bar = chart.bars.find( + ({ task: t }) => t.id === task.id, + ).$bar; + bar.style.fill = `hsla(${~~(360 * Math.random())}, 70%, 72%, 0.8)`; + }); + } + } + }, +}); + +const holidays = new Gantt('#holidays', tasks, { holidays: { - '#dcdce4': [], - '#a3e635': HOLIDAYS, + 'var(--g-weekend-highlight-color)': [], + '#fffddb': HOLIDAYS, }, ignore: ['weekend'], infinite_padding: false, + container_height: 350, scroll_to: daysSince(-7), }); -const styling = new Gantt('#styling', tasksMany, { - holidays: null, - arrow_curve: 6, - column_width: 32, - infinite_padding: true, -}); - -// Creates forms SWITCHES = { - 'mutability-form': { - 'readonly-progress': 'Progress', - 'readonly-dates': 'Dates', - 'readonly-general': 'Editable', - }, 'sideheader-form': { - 'toggle-today': 'Scroll to Today', - 'toggle-view-mode': 'Change View Mode', + 'toggle-today': 'Scroll to today: ', + 'toggle-view-mode': 'Change view mode: ', }, - 'holidays-form': { - 'toggle-weekends': ['Mark weekends', false], - 'ignore-weekends': 'Exclude weekends', + 'holiday-subform': { + 'toggle-weekends': ['Mark weekends: ', false], + 'ignore-weekends': 'Exclude weekends: ', }, }; +for (let form of ['sideheader-form', 'holiday-form']) { + let formNode = document.getElementById(form); + let parent = formNode.parentElement; + parent.appendChild(formNode); +} + for (let form in SWITCHES) { for (let id in SWITCHES[form]) { createSwitch(form, id, SWITCHES[form][id]); } } -// Manipulation - const UPDATES = [ - [ - mutablity, - { - 'readonly-general': 'opp__readonly', - 'readonly-dates': 'opp__readonly_dates', - 'readonly-progress': 'opp__readonly_progress', - }, - (id, val) => { - if (id === 'readonly-general') { - document.getElementById('readonly-dates').checked = !val; - document.getElementById('readonly-progress').checked = !val; - } - }, - ], [ sideheader, { @@ -256,15 +244,16 @@ const UPDATES = [ { 'toggle-weekends': (val, opts) => ({ holidays: { - '#a3e635': opts.holidays['#a3e635'], - '#dcdce4': val ? 'weekend' : [], + '#fffddb': opts.holidays['#fffddb'], + 'var(--g-weekend-highlight-color)': val ? 'weekend' : [], }, ignore: [], }), 'declare-holiday': (val, opts) => ({ holidays: { - '#a3e635': [...HOLIDAYS, { date: val, name: 'Kay' }], - '#dcdce4': opts.holidays['#dcdce4'], + '#fffddb': [...HOLIDAYS, { date: val, name: 'Kay' }], + 'var(--g-weekend-highlight-color)': + opts.holidays['var(--g-weekend-highlight-color)'], }, }), 'ignore-weekends': (val, opts) => ({ @@ -272,7 +261,7 @@ const UPDATES = [ opts.ignore.filter((k) => k !== 'weekend')[0], ...(val ? ['weekend'] : []), ], - holidays: { '#a3e635': opts.holidays['#a3e635'] }, + holidays: { '#fffddb': opts.holidays['#fffddb'] }, }), 'declare-ignore': (val, opts) => ({ ignore: [ @@ -291,53 +280,13 @@ const UPDATES = [ } }, ], - [ - styling, - { - 'arrow-curve': 'arrow_curve', - 'column-width': 'column_width', - }, - ], ]; -const BUTTONS = { - 'radius-s': { bar_corner_radius: 3 }, - 'radius-m': { bar_corner_radius: 7 }, - 'radius-l': { bar_corner_radius: 14 }, - 'height-s': { bar_height: 20 }, - 'height-m': { bar_height: 30 }, - 'height-l': { bar_height: 45 }, - 'curve-s': { arrow_curve: 2 }, - 'curve-m': { bar_height: 5 }, - 'curve-l': { bar_height: 10 }, - 'width-s': { column_width: 25 }, - 'width-m': { column_width: 32 }, - 'width-l': { column_width: 50 }, - 'padding-s': { padding: 18 }, - 'padding-m': { padding: 30 }, - 'padding-l': { padding: 45 }, -}; - -for (let id in BUTTONS) { - let el = document.getElementById(id); - el.onclick = (e) => { - e.preventDefault(); - styling.update_options(BUTTONS[id]); - for (let k of document.querySelectorAll('.selected')) { - if (k.id.startsWith(el.id.split('-')[0])) { - k.classList.remove('selected'); - } - } - e.currentTarget.classList.add('selected'); - }; -} - for (let [chart, details, after] of UPDATES) { for (let id in details) { let el = document.getElementById(id); el.onchange = (e) => { - console.log('changed', e.currentTarget.id, e.currentTarget.value); let label = details[id]; let val; if (e.currentTarget.type === 'checkbox') { diff --git a/demo.js b/demo.js deleted file mode 100644 index d2953d1..0000000 --- a/demo.js +++ /dev/null @@ -1,365 +0,0 @@ -// Creates forms -SWITCHES = { - 'mutability-form': { - 'readonly-progress': 'Progress', - 'readonly-dates': 'Dates', - 'readonly-general': 'Editable', - }, - 'sideheader-form': { - 'toggle-today': 'Scroll to Today', - 'toggle-view-mode': 'Change View Mode', - }, - 'holidays-form': { - 'ignore-weekends': 'Exclude weekends', - 'toggle-weekends': ['Mark weekends', false], - }, -}; - -for (let form in SWITCHES) { - for (let id in SWITCHES[form]) { - createSwitch(form, id, SWITCHES[form][id]); - } -} - -let today = new Date(); -today.setHours(0, 0, 0, 0); -today = today.valueOf(); - -function random(begin = 10, end = 90, multiple = 10) { - let k; - do { - k = Math.floor(Math.random() * 100); - } while (k < begin || k > end || k % multiple !== 0); - return k; -} -const daysSince = (dx) => new Date(today + dx * 86400000); -const tasks = [ - { - start: daysSince(-2), - end: daysSince(2), - name: 'Redesign website', - id: 'Task 0', - progress: random(), - }, - { - start: daysSince(3), - duration: '6d', - name: 'Write new content', - id: 'Task 1', - progress: random(), - important: true, - dependencies: 'Task 0', - }, - { - start: daysSince(4), - duration: '2d', - name: 'Apply new styles', - id: 'Task 2', - progress: random(), - }, - { - start: daysSince(-4), - end: daysSince(0), - name: 'Review', - id: 'Task 3', - progress: random(), - }, -]; - -const tasksSpread = [ - { - start: daysSince(-30), - end: daysSince(-10), - name: 'Redesign website', - id: 'Task 0', - progress: random(), - }, - { - start: daysSince(-15), - duration: '21d', - name: 'Write new content', - id: 'Task 1', - progress: random(), - important: true, - }, - { - start: daysSince(10), - duration: '14d', - name: 'Review', - id: 'Task 3', - progress: random(), - }, - { - start: daysSince(-3), - duration: '4d', - name: 'Publish', - id: 'Task 4', - progress: random(), - }, -]; - -let tasksMany = [ - { - start: daysSince(-7), - end: daysSince(-5), - name: 'Initial brainstorming', - id: 'Task 0', - progress: random(), - }, - { - start: daysSince(-3), - end: daysSince(1), - name: 'Develop wireframe', - id: 'Task 1', - progress: random(), - dependencies: 'Task 0', - }, - { - start: daysSince(-1), - duration: '4d', - name: 'Client meeting', - id: 'Task 2', - progress: random(), - important: true, - }, - { - start: daysSince(1), - duration: '7d', - name: 'Create prototype', - id: 'Task 3', - dependencies: 'Task 2', - progress: random(), - }, - { - start: daysSince(3), - duration: '5d', - name: 'Test design with users', - dependencies: 'Task 2', - id: 'Task 4', - progress: random(), - important: true, - }, - { - start: daysSince(5), - end: daysSince(10), - name: 'Write technical documentation', - id: 'Task 5', - progress: random(), - }, - { - start: daysSince(8), - duration: '3d', - name: 'Prepare demo', - id: 'Task 6', - progress: random(), - }, - { - start: daysSince(10), - end: daysSince(12), - name: 'Final client review', - id: 'Task 7', - progress: random(), - important: true, - }, - { - start: daysSince(14), - duration: '6d', - name: 'Implement feedback', - id: 'Task 8', - progress: random(), - }, - { - start: daysSince(16), - duration: '4d', - name: 'Launch website', - id: 'Task 9', - progress: random(), - important: true, - }, -]; - -const HOLIDAYS = [ - { name: 'Republic Day', date: '2024-01-26' }, - { name: 'Maha Shivratri', date: '2024-02-23' }, - { name: 'Holi', date: '2024-03-11' }, - { name: 'Mahavir Jayanthi', date: '2024-04-07' }, - { name: 'Good Friday', date: '2024-04-10' }, - { name: 'May Day', date: '2024-05-01' }, - { name: 'Buddha Purnima', date: '2024-05-08' }, - { name: 'Krishna Janmastami', date: '2024-08-14' }, - { name: 'Independence Day', date: '2024-08-15' }, - { name: 'Ganesh Chaturthi', date: '2024-08-23' }, - { name: 'Id-Ul-Fitr', date: '2024-09-21' }, - { name: 'Vijaya Dashami', date: '2024-09-28' }, - { name: 'Mahatma Gandhi Jayanti', date: '2024-10-02' }, - { name: 'Diwali', date: '2024-10-17' }, - { name: 'Guru Nanak Jayanthi', date: '2024-11-02' }, - { name: 'Christmas', date: '2024-12-25' }, -]; - -const mutablity = new Gantt('#mutability', tasks, { - holidays: null, - scroll_to: daysSince(-7), - popup: () => false, -}); - -const sideheader = new Gantt('#sideheader', tasks, { - holidays: null, - view_mode_select: true, - scroll_to: null, -}); - -const holidays = new Gantt('#holidays', tasksSpread, { - holidays: { - '#bfdbfe': [], - '#a3e635': HOLIDAYS, - }, - ignore: ['weekend'], - scroll_to: daysSince(-7), -}); - -const styling = new Gantt('#styling', tasksMany, { - holidays: null, - arrow_curve: 6, - column_width: 32, - scroll_to: daysSince(-10), -}); - -const UPDATES = [ - [ - mutablity, - { - 'readonly-general': 'opp__readonly', - 'readonly-dates': 'opp__readonly_dates', - 'readonly-progress': 'opp__readonly_progress', - }, - (id, val) => { - if (id === 'readonly-general') { - document.getElementById('readonly-dates').checked = !val; - document.getElementById('readonly-progress').checked = !val; - } - }, - ], - [ - sideheader, - { - 'toggle-today': 'today_button', - 'toggle-view-mode': 'view_mode_select', - }, - ], - [ - holidays, - { - 'toggle-weekends': (val, opts) => ({ - holidays: { - '#a3e635': opts.holidays['#a3e635'], - '#bfdbfe': val ? 'weekend' : [], - }, - ignore: [], - }), - 'declare-holiday': (val, opts) => ({ - holidays: { - '#a3e635': [...HOLIDAYS, { date: val, name: 'Kay' }], - '#bfdbfe': opts.holidays['#bfdbfe'], - }, - }), - 'ignore-weekends': (val, opts) => ({ - ignore: [ - opts.ignore.filter((k) => k !== 'weekend')[0], - ...(val ? ['weekend'] : []), - ], - holidays: { '#a3e635': opts.holidays['#a3e635'] }, - }), - 'declare-ignore': (val, opts) => ({ - ignore: [ - ...(opts.ignore.includes('weekend') ? ['weekend'] : []), - val, - ], - }), - }, - (id, val) => { - let el = document.getElementById(id); - if (id === 'toggle-weekends' && val) { - document.getElementById('ignore-weekends').checked = false; - } - if (id === 'ignore-weekends' && val) { - document.getElementById('toggle-weekends').checked = false; - } - }, - ], - [ - styling, - { - 'arrow-curve': 'arrow_curve', - 'column-width': 'column_width', - }, - ], -]; - -const BUTTONS = { - 'radius-s': { bar_corner_radius: 3 }, - 'radius-m': { bar_corner_radius: 7 }, - 'radius-l': { bar_corner_radius: 14 }, - 'height-s': { bar_height: 20 }, - 'height-m': { bar_height: 30 }, - 'height-l': { bar_height: 45 }, - 'padding-s': { padding: 18 }, - 'padding-m': { padding: 30 }, - 'padding-l': { padding: 45 }, -}; - -for (let id in BUTTONS) { - let el = document.getElementById(id); - el.onclick = (e) => { - e.preventDefault(); - styling.update_options(BUTTONS[id]); - for (let k of document.querySelectorAll('.selected')) { - if (k.id.startsWith(el.id.split('-')[0])) { - k.classList.remove('selected'); - } - } - e.currentTarget.classList.add('selected'); - }; -} - -for (let [chart, details, after] of UPDATES) { - for (let id in details) { - let el = document.getElementById(id); - - el.onchange = (e) => { - let label = details[id]; - let val; - if (e.currentTarget.type === 'checkbox') { - if (typeof label === 'string') { - let opposite = label.slice(0, 5) === 'opp__'; - if (opposite) label = label.slice(5); - val = opposite - ? !e.currentTarget.checked - : e.currentTarget.checked; - } else if (typeof label === 'object') { - val = label[e.currentTarget.checked ? 1 : 2]; - label = label[0]; - } else { - val = - e.currentTarget.type === 'checkbox' - ? e.currentTarget.checked - : e.currentTarget.value; - } - } else { - val = - e.currentTarget.type === 'date' - ? e.currentTarget.value - : +e.currentTarget.value; - } - - if (typeof label === 'function') { - chart.update_options(label(val, chart.options)); - } else { - chart.update_options({ - [label]: val, - }); - } - after && after(id, val, chart); - }; - } -} diff --git a/package.json b/package.json index 5200b6a..e6511d8 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,10 @@ { "name": "frappe-gantt", - "version": "0.9.0", + "version": "1.0.0", "description": "A simple, modern, interactive gantt library for the web", "main": "src/index.js", "type": "module", "scripts": { - "start": "yarn run dev", "dev": "vite", "build-dev": "vite build --watch", "build": "vite build",