mirror of https://github.com/Budibase/budibase.git
18 changed files with 644 additions and 4998 deletions
@ -1,140 +1,30 @@ |
|||
import * as path from "path" |
|||
import svelte from "rollup-plugin-svelte-hot" |
|||
import svelte from "rollup-plugin-svelte" |
|||
import resolve from "@rollup/plugin-node-resolve" |
|||
import commonjs from "@rollup/plugin-commonjs" |
|||
import json from "@rollup/plugin-json" |
|||
import copy from "rollup-plugin-copy" |
|||
import hmr from "rollup-plugin-hot" |
|||
import del from "rollup-plugin-delete" |
|||
import { terser } from "rollup-plugin-terser" |
|||
import postcss from "rollup-plugin-postcss" |
|||
import { plugin as Svench } from "svench/rollup" |
|||
import builtins from "rollup-plugin-node-builtins" |
|||
|
|||
const WATCH = !!process.env.ROLLUP_WATCH |
|||
const SVENCH = !!process.env.SVENCH |
|||
const HOT = WATCH |
|||
const PRODUCTION = !WATCH |
|||
|
|||
const svench = Svench({ |
|||
// The root dir that Svench will parse and watch.
|
|||
//
|
|||
// NOTE Watching the root of the project, to let Svench render *.md for us.
|
|||
//
|
|||
// NOTE By default, `node_modules` and `.git` dirs are ignored. This can be
|
|||
// customized by passing a function to `ignore` option. Default ignore is:
|
|||
//
|
|||
// ignore: path => /(?:^|\/)(?:node_modules|\.git)\//.test(path),
|
|||
//
|
|||
dir: ".", |
|||
|
|||
// Make `src` dir a section (that is, it will always be "expanded" in the
|
|||
// menu).
|
|||
autoSections: ["src"], |
|||
|
|||
// Use custom index.html
|
|||
index: { |
|||
source: "public/index.html", |
|||
}, |
|||
|
|||
extensions: [".svench", ".svench.svelte", ".svench.svx", ".md"], |
|||
|
|||
serve: WATCH && { |
|||
host: "0.0.0.0", |
|||
port: 4242, |
|||
public: "public", |
|||
nollup: "0.0.0.0:42421", |
|||
const makeConfig = ({ input, name }) => ({ |
|||
input, |
|||
output: { |
|||
sourcemap: true, |
|||
format: "esm", |
|||
file: `dist/${name}.es.js`, |
|||
}, |
|||
plugins: [ |
|||
resolve(), |
|||
commonjs(), |
|||
svelte({ |
|||
emitCss: true, |
|||
}), |
|||
postcss(), |
|||
terser(), |
|||
json(), |
|||
], |
|||
}) |
|||
|
|||
// NOTE configs are in function form to avoid instantiating plugins of the
|
|||
// config that is not used for nothing (in particular, the HMR plugin launches
|
|||
// a dev server on startup, this is not desired when just building for prod)
|
|||
const configs = { |
|||
svench: () => ({ |
|||
input: ".svench/svench.js", |
|||
output: { |
|||
format: "es", |
|||
dir: "public/svench", |
|||
}, |
|||
plugins: [ |
|||
builtins(), |
|||
|
|||
// NOTE cleaning old builds is required to avoid serving stale static
|
|||
// files from a previous build instead of in-memory files from the dev/hmr
|
|||
// server
|
|||
del({ |
|||
targets: "public/svench/*", |
|||
runOnce: true, |
|||
}), |
|||
|
|||
postcss({ |
|||
hot: HOT, |
|||
extract: path.resolve("public/svench/theme.css"), |
|||
sourceMap: true, |
|||
}), |
|||
|
|||
svench, |
|||
|
|||
svelte({ |
|||
dev: !PRODUCTION, |
|||
extensions: [".svelte", ".svench", ".svx", ".md"], |
|||
// Svench's "combined" preprocessor wraps both Mdsvex preprocessors
|
|||
// (configured for Svench), and its own preprocessor (for static
|
|||
// analysis -- eg extract source from views)
|
|||
preprocess: svench.$.preprocess, |
|||
hot: HOT && { |
|||
optimistic: true, |
|||
noPreserveState: false, |
|||
}, |
|||
}), |
|||
|
|||
resolve({ browser: true }), |
|||
|
|||
commonjs(), |
|||
json(), |
|||
|
|||
HOT && |
|||
hmr({ |
|||
host: "0.0.0.0", |
|||
public: "public", |
|||
inMemory: true, |
|||
compatModuleHot: !HOT, // for terser
|
|||
}), |
|||
], |
|||
|
|||
watch: { |
|||
clearScreen: false, |
|||
// buildDelay is needed to ensure Svench's code (routes) generator will
|
|||
// pick file changes before Rollup and prevent a double build (if Rollup
|
|||
// first sees a change to src/Foo.svench, then to Svench's routes.js)
|
|||
buildDelay: 100, |
|||
}, |
|||
}), |
|||
|
|||
lib: () => ({ |
|||
input: "src/index.js", |
|||
output: [{ file: "dist/bundle.mjs", format: "es" }], |
|||
plugins: [ |
|||
svelte({ |
|||
dev: !PRODUCTION, |
|||
extensions: [".svelte"], |
|||
emitCss: true, |
|||
}), |
|||
postcss(), |
|||
copy({ |
|||
targets: [ |
|||
{ |
|||
src: ".svench/svench.css", |
|||
dest: "public", |
|||
rename: "global.css", |
|||
}, |
|||
], |
|||
}), |
|||
resolve(), |
|||
commonjs(), |
|||
json(), |
|||
], |
|||
}), |
|||
} |
|||
|
|||
export default configs[SVENCH ? "svench" : "lib"]() |
|||
export default [ |
|||
makeConfig({ input: "src/index.js", name: "bbui" }), |
|||
makeConfig({ input: "src/Form/internal/index.js", name: "internal" }), |
|||
] |
|||
|
|||
@ -0,0 +1,40 @@ |
|||
<script> |
|||
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
|||
import FieldLabel from "./FieldLabel.svelte" |
|||
|
|||
export let id = null |
|||
export let label = null |
|||
export let labelPosition = "above" |
|||
export let disabled = false |
|||
export let error = null |
|||
</script> |
|||
|
|||
<div class="spectrum-Form-item" class:above={labelPosition === 'above'}> |
|||
<FieldLabel forId={id} {label} position={labelPosition} /> |
|||
<div class="spectrum-Form-itemField"> |
|||
<slot /> |
|||
{#if error} |
|||
<div class="error">{error}</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.spectrum-Form-item.above { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
.spectrum-Form-itemField { |
|||
position: relative; |
|||
width: 100%; |
|||
} |
|||
|
|||
.error { |
|||
color: var( |
|||
--spectrum-semantic-negative-color-default, |
|||
var(--spectrum-global-color-red-500) |
|||
); |
|||
font-size: var(--spectrum-global-dimension-font-size-75); |
|||
margin-top: var(--spectrum-global-dimension-size-75); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,26 @@ |
|||
<script> |
|||
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
|||
|
|||
export let forId |
|||
export let label |
|||
export let position = "above" |
|||
|
|||
$: className = position === "above" ? "" : `spectrum-FieldLabel--${position}` |
|||
</script> |
|||
|
|||
<label |
|||
for={forId} |
|||
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${className}`}> |
|||
{label || ''} |
|||
</label> |
|||
|
|||
<style> |
|||
label { |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.spectrum-FieldLabel--right, |
|||
.spectrum-FieldLabel--left { |
|||
padding-right: var(--spectrum-global-dimension-size-200); |
|||
} |
|||
</style> |
|||
@ -1,190 +1,29 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import TextField from "./internal/TextField.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
import Button from "../Button/Button.svelte" |
|||
import Label from "../Styleguide/Label.svelte" |
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let name = undefined |
|||
export let label = undefined |
|||
export let outline = false |
|||
export let presentation = false |
|||
export let thin = false |
|||
export let extraThin = false |
|||
export let large = false |
|||
export let border = false |
|||
export let edit = false |
|||
export let value = null |
|||
export let label = null |
|||
export let labelPosition = "above" |
|||
export let placeholder = null |
|||
export let type = "text" |
|||
export let disabled = false |
|||
export let type = undefined |
|||
export let placeholder = "" |
|||
export let value = "" |
|||
export let error = false |
|||
export let validator = () => {} |
|||
|
|||
// This section handles the edit mode and dispatching of things to the parent when saved |
|||
let editMode = false |
|||
export let error = null |
|||
|
|||
const updateValue = e => { |
|||
if (type === "number") { |
|||
const num = parseFloat(e.target.value) |
|||
value = isNaN(num) ? "" : num |
|||
} else { |
|||
value = e.target.value |
|||
} |
|||
} |
|||
|
|||
const save = () => { |
|||
editMode = false |
|||
dispatch("save", value) |
|||
} |
|||
|
|||
const enableEdit = () => { |
|||
editMode = true |
|||
const dispatch = createEventDispatcher() |
|||
const onChange = e => { |
|||
dispatch("change", e.detail) |
|||
value = e.detail |
|||
} |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
{#if label || edit} |
|||
<div class="label-container"> |
|||
{#if label} |
|||
<Label extraSmall grey forAttr={name}>{label}</Label> |
|||
{/if} |
|||
{#if edit} |
|||
<div class="controls"> |
|||
<Button small secondary disabled={editMode} on:click={enableEdit}> |
|||
Edit |
|||
</Button> |
|||
<Button small blue disabled={!editMode} on:click={save}>Save</Button> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
{/if} |
|||
<input |
|||
class:outline |
|||
class:presentation |
|||
class:thin |
|||
class:extraThin |
|||
class:large |
|||
class:border |
|||
on:change |
|||
on:input |
|||
on:change={updateValue} |
|||
on:input={updateValue} |
|||
on:blur={updateValue} |
|||
use:validator |
|||
disabled={disabled || (edit && !editMode)} |
|||
value={value == null ? '' : value} |
|||
<Field {label} {labelPosition} {disabled} {error}> |
|||
<TextField |
|||
{error} |
|||
{disabled} |
|||
{value} |
|||
{placeholder} |
|||
{type} |
|||
{name} |
|||
{placeholder} /> |
|||
{#if error} |
|||
<div class="error">{error}</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
min-width: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.label-container { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: flex-end; |
|||
margin-bottom: var(--spacing-s); |
|||
} |
|||
.label-container :global(label) { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.controls { |
|||
align-items: center; |
|||
display: grid; |
|||
grid-template-columns: auto auto; |
|||
grid-gap: 12px; |
|||
margin-left: auto; |
|||
padding-left: 12px; |
|||
} |
|||
.controls :global(button) { |
|||
min-width: 100px; |
|||
font-size: var(--font-size-s); |
|||
border-radius: var(--rounded-small); |
|||
} |
|||
|
|||
input { |
|||
min-width: 0; |
|||
box-sizing: border-box; |
|||
color: var(--ink); |
|||
font-size: var(--font-size-s); |
|||
border-radius: var(--border-radius-s); |
|||
border: none; |
|||
background-color: var(--grey-2); |
|||
padding: var(--spacing-m); |
|||
margin: 0; |
|||
outline: none; |
|||
font-family: var(--font-sans); |
|||
border: var(--border-transparent); |
|||
transition: all 0.2s ease-in-out; |
|||
} |
|||
input.presentation { |
|||
background-color: var(--background); |
|||
border: var(--background) 2px solid; |
|||
} |
|||
input.presentation:hover { |
|||
background-color: var(--grey-2); |
|||
border: var(--grey-4) 2px solid; |
|||
} |
|||
input.thin { |
|||
font-size: var(--font-size-xs); |
|||
} |
|||
input.extraThin { |
|||
font-size: var(--font-size-xs); |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
} |
|||
input.large { |
|||
font-size: var(--font-size-m); |
|||
padding: var(--spacing-l); |
|||
} |
|||
input.border { |
|||
border: var(--border-grey-2); |
|||
} |
|||
input.border:active { |
|||
border: var(--border-blue); |
|||
} |
|||
input.border:focus { |
|||
border: var(--border-blue); |
|||
} |
|||
input.outline { |
|||
border: var(--border-light-2); |
|||
background: var(--background); |
|||
} |
|||
input.outline:active { |
|||
border: var(--border-blue); |
|||
} |
|||
input.outline:focus { |
|||
border: var(--border-blue); |
|||
} |
|||
input:hover { |
|||
border: var(--grey-4) 2px solid; |
|||
} |
|||
input::placeholder { |
|||
color: var(--grey-6); |
|||
} |
|||
input:focus { |
|||
border: var(--border-blue); |
|||
} |
|||
input:disabled { |
|||
background: var(--grey-4); |
|||
color: var(--grey-6); |
|||
} |
|||
|
|||
.error { |
|||
margin-top: 10px; |
|||
font-size: var(--font-size-xs); |
|||
font-family: var(--font-sans); |
|||
line-height: 1.17; |
|||
color: var(--red); |
|||
} |
|||
</style> |
|||
on:change={onChange} /> |
|||
</Field> |
|||
|
|||
@ -1,324 +1,33 @@ |
|||
<script> |
|||
import Portal from "svelte-portal" |
|||
import { afterUpdate } from "svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
import { fly } from "svelte/transition" |
|||
import Label from "../Styleguide/Label.svelte" |
|||
const xPath = |
|||
"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" |
|||
import Multiselect from "./internal/Multiselect.svelte" |
|||
import Field from "./Field.svelte" |
|||
|
|||
import positionDropdown from "../Actions/position_dropdown" |
|||
import clickOutside from "../Actions/click_outside" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = [] |
|||
export let value = null |
|||
export let label = undefined |
|||
export let align = "left" |
|||
export let secondary = false |
|||
export let outline = false |
|||
export let disabled = false |
|||
export let placeholder = undefined |
|||
export let extraThin = false |
|||
|
|||
let options = [] |
|||
let optionsVisible = false |
|||
let slot |
|||
let anchor |
|||
$: lookupMap = mapValues(value) |
|||
$: selectedOptions = options.filter(option => lookupMap[option.value]) |
|||
|
|||
afterUpdate(() => { |
|||
// Update available options |
|||
const domOptions = Array.from(slot.querySelectorAll("option")) |
|||
options = domOptions.map(option => ({ |
|||
value: option.value, |
|||
name: option.textContent, |
|||
})) |
|||
}) |
|||
|
|||
function mapValues(value) { |
|||
let map = {} |
|||
if (value) { |
|||
value.forEach(option => { |
|||
map[option] = true |
|||
}) |
|||
} |
|||
return map |
|||
} |
|||
export let labelPosition = "above" |
|||
export let error = null |
|||
export let placeholder = null |
|||
export let options = [] |
|||
export let getOptionLabel = option => option |
|||
export let getOptionValue = option => option |
|||
|
|||
function add(val) { |
|||
value = [...value, val] |
|||
dispatch("change", value) |
|||
} |
|||
|
|||
function remove(val) { |
|||
value = value.filter(option => option !== val) |
|||
dispatch("change", value) |
|||
} |
|||
|
|||
function showOptions(show) { |
|||
optionsVisible = show |
|||
} |
|||
|
|||
function handleClick() { |
|||
showOptions(!optionsVisible) |
|||
} |
|||
|
|||
function handleOptionMousedown(e) { |
|||
const value = e.target.dataset.value |
|||
if (value == null) { |
|||
return |
|||
} |
|||
if (lookupMap[value]) { |
|||
remove(value) |
|||
} else { |
|||
add(value) |
|||
} |
|||
const dispatch = createEventDispatcher() |
|||
const onChange = e => { |
|||
dispatch("change", e.detail) |
|||
value = e.detail |
|||
} |
|||
</script> |
|||
|
|||
{#if label} |
|||
<Label extraSmall grey>{label}</Label> |
|||
{/if} |
|||
<div class="multiselect" bind:this={anchor}> |
|||
<div class="tokens-wrapper"> |
|||
<div |
|||
class="tokens" |
|||
class:outline |
|||
class:disabled |
|||
class:secondary |
|||
class:extraThin |
|||
class:optionsVisible |
|||
on:click|self={handleClick} |
|||
class:empty={!value || !value.length}> |
|||
{#each selectedOptions as option} |
|||
<div |
|||
class="token" |
|||
class:extraThin |
|||
data-id={option.value} |
|||
on:click|self={handleClick}> |
|||
<span>{option.name}</span> |
|||
<div |
|||
class="token-remove" |
|||
title="Remove {option.name}" |
|||
on:click={() => remove(option.value)}> |
|||
<svg |
|||
class="icon-clear" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
width="18" |
|||
height="18" |
|||
viewBox="0 0 24 24"> |
|||
<path d={xPath} /> |
|||
</svg> |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
{#if !value || !value.length} |
|||
{#if placeholder && placeholder.length} |
|||
<div class:disabled class="placeholder">{placeholder}</div> |
|||
{:else} |
|||
<div class="placeholder"> </div> |
|||
{/if} |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
<select bind:this={slot} type="multiple" class="hidden"> |
|||
<slot /> |
|||
</select> |
|||
|
|||
{#if optionsVisible} |
|||
<Portal> |
|||
<ul |
|||
class="options" |
|||
use:positionDropdown={{ anchor, align }} |
|||
use:clickOutside={() => showOptions(false)} |
|||
transition:fly={{ duration: 200, y: 5 }} |
|||
on:mousedown|preventDefault={handleOptionMousedown}> |
|||
{#each options as option} |
|||
<li |
|||
class:selected={lookupMap[option.value]} |
|||
data-value={option.value}> |
|||
{option.name} |
|||
</li> |
|||
{/each} |
|||
{#if !options.length} |
|||
<li class="no-results">No results</li> |
|||
{/if} |
|||
</ul> |
|||
</Portal> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.multiselect { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
font-family: var(--font-sans); |
|||
min-width: 0; |
|||
} |
|||
.multiselect:hover { |
|||
border-bottom-color: hsl(0, 0%, 50%); |
|||
} |
|||
|
|||
.tokens-wrapper { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
flex: 0 1 auto; |
|||
} |
|||
|
|||
.tokens { |
|||
align-items: center; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
position: relative; |
|||
width: 0; |
|||
flex: 1 1 auto; |
|||
background-color: var(--background); |
|||
border-radius: var(--border-radius-m); |
|||
padding: 0 var(--spacing-m) calc(var(--spacing-m) - var(--spacing-xs)) |
|||
calc(var(--spacing-m) / 2); |
|||
border: var(--border-transparent); |
|||
} |
|||
.tokens.disabled { |
|||
background-color: var(--grey-4); |
|||
pointer-events: none; |
|||
} |
|||
.tokens.outline { |
|||
border: var(--border-dark); |
|||
} |
|||
.tokens.secondary { |
|||
background-color: var(--grey-2); |
|||
} |
|||
.tokens.extraThin { |
|||
padding: 0 var(--spacing-m) calc(var(--spacing-s) - var(--spacing-xs)) |
|||
calc(var(--spacing-m) / 2); |
|||
} |
|||
.tokens:hover { |
|||
cursor: pointer; |
|||
} |
|||
.tokens.optionsVisible { |
|||
border: var(--border-blue); |
|||
} |
|||
.tokens.empty { |
|||
padding: var(--spacing-m); |
|||
font-size: var(--font-size-xs); |
|||
user-select: none; |
|||
} |
|||
.tokens.empty.extraThin { |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
} |
|||
.tokens::after { |
|||
width: 100%; |
|||
left: 0; |
|||
} |
|||
|
|||
.token { |
|||
font-size: var(--font-size-xs); |
|||
background-color: var(--ink); |
|||
color: var(--background); |
|||
border-radius: var(--border-radius-l); |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
margin: calc(var(--spacing-m) - var(--spacing-xs)) 0 0 |
|||
calc(var(--spacing-m) / 2); |
|||
max-height: 1.3rem; |
|||
padding: var(--spacing-xs) var(--spacing-s); |
|||
transition: background-color 0.3s; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
} |
|||
.token.extraThin { |
|||
margin: calc(var(--spacing-s) - var(--spacing-xs)) 0 0 |
|||
calc(var(--spacing-m) / 2); |
|||
} |
|||
.token span { |
|||
pointer-events: none; |
|||
user-select: none; |
|||
flex: 1 1 auto; |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
} |
|||
.token .token-remove { |
|||
align-items: center; |
|||
background-color: var(--grey-7); |
|||
border-radius: 50%; |
|||
display: flex; |
|||
justify-content: center; |
|||
height: 1rem; |
|||
width: 1rem; |
|||
margin: calc(-1 * var(--spacing-xs)) 0 calc(-1 * var(--spacing-xs)) |
|||
var(--spacing-xs); |
|||
flex: 0 0 auto; |
|||
} |
|||
.token path { |
|||
fill: var(--background); |
|||
} |
|||
.token .token-remove:hover { |
|||
background-color: var(--grey-6); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.placeholder { |
|||
pointer-events: none; |
|||
color: var(--ink); |
|||
} |
|||
.placeholder.disabled { |
|||
color: var(--grey-6); |
|||
} |
|||
|
|||
.icon-clear path { |
|||
fill: white; |
|||
} |
|||
.options { |
|||
left: 0; |
|||
list-style: none; |
|||
margin-block-end: 0; |
|||
margin-block-start: 0; |
|||
overflow-y: auto; |
|||
padding-inline-start: 0; |
|||
position: absolute; |
|||
border: var(--border-dark); |
|||
border-radius: var(--border-radius-m); |
|||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15); |
|||
margin: var(--spacing-xs) 0; |
|||
padding: var(--spacing-s) 0; |
|||
background-color: var(--background); |
|||
max-height: 200px; |
|||
} |
|||
li { |
|||
cursor: pointer; |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
font-size: var(--font-size-xs); |
|||
color: var(--ink); |
|||
} |
|||
li.selected { |
|||
background-color: var(--blue); |
|||
color: white; |
|||
} |
|||
li:not(.selected):hover { |
|||
background-color: var(--grey-1); |
|||
} |
|||
li.no-results:hover { |
|||
background-color: white; |
|||
cursor: initial; |
|||
} |
|||
|
|||
.hidden { |
|||
height: 0; |
|||
overflow: hidden; |
|||
visibility: hidden; |
|||
padding: 0; |
|||
margin: 0; |
|||
border: 0; |
|||
outline: 0; |
|||
} |
|||
</style> |
|||
<Field {label} {labelPosition} {disabled} {error}> |
|||
<Multiselect |
|||
{error} |
|||
{disabled} |
|||
{value} |
|||
{options} |
|||
{placeholder} |
|||
{getOptionLabel} |
|||
{getOptionValue} |
|||
on:change={onChange} /> |
|||
</Field> |
|||
|
|||
@ -1,95 +1,33 @@ |
|||
<script> |
|||
import Icon from "../Icons/Icon.svelte" |
|||
import Label from "../Styleguide/Label.svelte" |
|||
import Field from "./Field.svelte" |
|||
import Select from "./internal/Select.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = "" |
|||
export let name = undefined |
|||
export let value = null |
|||
export let label = undefined |
|||
export let thin = false |
|||
export let extraThin = false |
|||
export let secondary = false |
|||
export let outline = false |
|||
export let disabled = false |
|||
</script> |
|||
|
|||
<div> |
|||
{#if label} |
|||
<Label extraSmall grey forAttr={name}>{label}</Label> |
|||
{/if} |
|||
<div class="relative"> |
|||
<select |
|||
{name} |
|||
class:thin |
|||
class:extraThin |
|||
class:secondary |
|||
class:outline |
|||
{disabled} |
|||
on:change |
|||
bind:value> |
|||
<slot /> |
|||
</select> |
|||
<div class="pointer"> |
|||
<Icon name="arrowdown" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
select { |
|||
font-family: var(--font-sans); |
|||
display: block !important; |
|||
width: 100% !important; |
|||
border-radius: var(--border-radius-s); |
|||
border: none; |
|||
text-align: left; |
|||
color: var(--ink); |
|||
font-size: var(--font-size-s); |
|||
padding: var(--spacing-m) 2rem var(--spacing-m) var(--spacing-m) !important; |
|||
appearance: none !important; |
|||
-webkit-appearance: none !important; |
|||
-moz-appearance: none !important; |
|||
align-items: center; |
|||
white-space: pre; |
|||
outline: none; |
|||
border: var(--border-transparent); |
|||
background-color: var(--background); |
|||
} |
|||
select.thin { |
|||
padding: var(--spacing-m); |
|||
font-size: var(--font-size-xs); |
|||
} |
|||
select.extraThin { |
|||
padding: var(--spacing-s) 2rem var(--spacing-s) var(--spacing-m) !important; |
|||
font-size: var(--font-size-xs); |
|||
} |
|||
select.secondary { |
|||
background: var(--grey-2); |
|||
} |
|||
select.outline { |
|||
border: var(--border-light-2); |
|||
} |
|||
select:focus { |
|||
border: var(--border-blue); |
|||
} |
|||
select:disabled { |
|||
background: var(--grey-4); |
|||
color: var(--grey-6); |
|||
} |
|||
export let labelPosition = "above" |
|||
export let error = null |
|||
export let placeholder = null |
|||
export let options = [] |
|||
export let getOptionLabel = option => option |
|||
export let getOptionValue = option => option |
|||
|
|||
.relative { |
|||
position: relative !important; |
|||
display: block; |
|||
const dispatch = createEventDispatcher() |
|||
const onChange = e => { |
|||
dispatch("change", e.detail) |
|||
value = e.detail |
|||
} |
|||
</script> |
|||
|
|||
.pointer { |
|||
right: 0 !important; |
|||
top: 0 !important; |
|||
bottom: 0 !important; |
|||
position: absolute !important; |
|||
pointer-events: none !important; |
|||
padding-left: 0.5rem !important; |
|||
align-items: center !important; |
|||
display: flex !important; |
|||
color: var(--ink); |
|||
} |
|||
</style> |
|||
<Field {label} {labelPosition} {disabled} {error}> |
|||
<Select |
|||
{error} |
|||
{disabled} |
|||
{value} |
|||
{options} |
|||
{placeholder} |
|||
{getOptionLabel} |
|||
{getOptionValue} |
|||
on:change={onChange} /> |
|||
</Field> |
|||
|
|||
@ -0,0 +1,61 @@ |
|||
<script> |
|||
import Picker from "./Picker.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = [] |
|||
export let fieldId = null |
|||
export let placeholder = null |
|||
export let disabled = false |
|||
export let error = null |
|||
export let options = [] |
|||
export let getOptionLabel = option => option |
|||
export let getOptionValue = option => option |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
$: fieldText = getFieldText(value) |
|||
$: valueLookupMap = getValueLookupMap(value) |
|||
$: isOptionSelected = option => valueLookupMap[option] === true |
|||
|
|||
const getFieldText = value => { |
|||
if (value?.length) { |
|||
const count = value?.length ?? 0 |
|||
return `${count} selected option${count === 1 ? "" : "s"}` |
|||
} else { |
|||
return placeholder || "Choose some options" |
|||
} |
|||
} |
|||
|
|||
const getValueLookupMap = value => { |
|||
let map = {} |
|||
if (value?.length) { |
|||
value.forEach(option => { |
|||
const optionValue = getOptionValue(option) |
|||
if (optionValue) { |
|||
map[optionValue] = true |
|||
} |
|||
}) |
|||
} |
|||
return map |
|||
} |
|||
|
|||
const toggleOption = option => { |
|||
if (valueLookupMap[option]) { |
|||
const filtered = value.filter(option => option !== id) |
|||
dispatch("change", filtered) |
|||
} else { |
|||
dispatch("change", [...value, option]) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Picker |
|||
{fieldId} |
|||
{error} |
|||
{disabled} |
|||
{fieldText} |
|||
{options} |
|||
isPlaceholder={!value?.length} |
|||
{isOptionSelected} |
|||
{getOptionLabel} |
|||
{getOptionValue} |
|||
onSelectOption={toggleOption} /> |
|||
@ -0,0 +1,109 @@ |
|||
<script> |
|||
import "@spectrum-css/picker/dist/index-vars.css" |
|||
import "@spectrum-css/popover/dist/index-vars.css" |
|||
import "@spectrum-css/menu/dist/index-vars.css" |
|||
import { fly } from "svelte/transition" |
|||
|
|||
export let fieldId = null |
|||
export let disabled = false |
|||
export let error = null |
|||
export let fieldText = "" |
|||
export let isPlaceholder = false |
|||
export let placeholderOption = null |
|||
export let options = [] |
|||
export let isOptionSelected = () => false |
|||
export let onSelectOption = () => {} |
|||
export let getOptionLabel = option => option |
|||
export let getOptionValue = option => option |
|||
export let open = false |
|||
</script> |
|||
|
|||
<button |
|||
id={fieldId} |
|||
class="spectrum-Picker spectrum-Picker--sizeM" |
|||
{disabled} |
|||
class:is-invalid={!!error} |
|||
class:is-open={open} |
|||
aria-haspopup="listbox" |
|||
on:click={() => (open = true)}> |
|||
<span class="spectrum-Picker-label" class:is-placeholder={isPlaceholder}> |
|||
{fieldText} |
|||
</span> |
|||
{#if error} |
|||
<svg |
|||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
aria-label="Folder"> |
|||
<use xlink:href="#spectrum-icon-18-Alert" /> |
|||
</svg> |
|||
{/if} |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-css-icon-Chevron100" /> |
|||
</svg> |
|||
</button> |
|||
{#if open} |
|||
<div class="overlay" on:mousedown|self={() => (open = false)} /> |
|||
<div |
|||
transition:fly={{ y: -20, duration: 200 }} |
|||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"> |
|||
<ul class="spectrum-Menu" role="listbox"> |
|||
{#if placeholderOption} |
|||
<li |
|||
class="spectrum-Menu-item" |
|||
class:is-selected={isPlaceholder} |
|||
role="option" |
|||
aria-selected="true" |
|||
tabindex="0" |
|||
on:click={() => onSelectOption(null)}> |
|||
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-css-icon-Checkmark100" /> |
|||
</svg> |
|||
</li> |
|||
{/if} |
|||
{#each options as option} |
|||
<li |
|||
class="spectrum-Menu-item" |
|||
class:is-selected={isOptionSelected(getOptionValue(option))} |
|||
role="option" |
|||
aria-selected="true" |
|||
tabindex="0" |
|||
on:click={() => onSelectOption(getOptionValue(option))}> |
|||
<span class="spectrum-Menu-itemLabel">{getOptionLabel(option)}</span> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-css-icon-Checkmark100" /> |
|||
</svg> |
|||
</li> |
|||
{/each} |
|||
</ul> |
|||
</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
.overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
z-index: 999; |
|||
} |
|||
.spectrum-Popover { |
|||
max-height: 240px; |
|||
width: 100%; |
|||
z-index: 999; |
|||
} |
|||
.spectrum-Picker { |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,42 @@ |
|||
<script> |
|||
import { createEventDispatcher } from "svelte" |
|||
import Picker from "./Picker.svelte" |
|||
|
|||
export let value = null |
|||
export let fieldId = null |
|||
export let placeholder = null |
|||
export let disabled = false |
|||
export let error = null |
|||
export let options = [] |
|||
export let getOptionLabel = option => option |
|||
export let getOptionValue = option => option |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
let open = false |
|||
$: placeholderText = placeholder || "Choose an option" |
|||
$: isNull = value == null || value === "" |
|||
$: selectedOption = options.find(option => getOptionValue(option) === value) |
|||
$: selectedLabel = selectedOption |
|||
? getOptionLabel(selectedOption) |
|||
: placeholderText |
|||
$: fieldText = isNull ? placeholderText : selectedLabel |
|||
|
|||
const selectOption = value => { |
|||
dispatch("change", value) |
|||
open = false |
|||
} |
|||
</script> |
|||
|
|||
<Picker |
|||
bind:open |
|||
{fieldId} |
|||
{error} |
|||
{disabled} |
|||
{fieldText} |
|||
{options} |
|||
{getOptionLabel} |
|||
{getOptionValue} |
|||
isPlaceholder={isNull} |
|||
placeholderOption={placeholderText} |
|||
isOptionSelected={option => option === value} |
|||
onSelectOption={selectOption} /> |
|||
@ -0,0 +1,60 @@ |
|||
<script> |
|||
import "@spectrum-css/textfield/dist/index-vars.css" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = "" |
|||
export let placeholder = "" |
|||
export let type = "text" |
|||
export let disabled = false |
|||
export let error = null |
|||
export let id = null |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
const updateValue = value => { |
|||
if (type === "number") { |
|||
const float = parseFloat(value) |
|||
value = isNaN(float) ? null : float |
|||
} |
|||
dispatch("change", value) |
|||
} |
|||
|
|||
const onBlur = event => { |
|||
updateValue(event.target.value) |
|||
} |
|||
|
|||
const updateValueOnEnter = event => { |
|||
if (event.key === "Enter") { |
|||
updateValue(event.target.value) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div |
|||
class="spectrum-Textfield" |
|||
class:is-invalid={!!error} |
|||
class:is-disabled={disabled}> |
|||
{#if error} |
|||
<svg |
|||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-icon-18-Alert" /> |
|||
</svg> |
|||
{/if} |
|||
<input |
|||
on:keyup={updateValueOnEnter} |
|||
{disabled} |
|||
{id} |
|||
value={value || ''} |
|||
placeholder={placeholder || ''} |
|||
on:blur={onBlur} |
|||
{type} |
|||
class="spectrum-Textfield-input" /> |
|||
</div> |
|||
|
|||
<style> |
|||
.spectrum-Textfield { |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,2 @@ |
|||
export { default as TextField } from "./TextField.svelte" |
|||
export { default as Select } from "./Select.svelte" |
|||
@ -1,22 +0,0 @@ |
|||
import svelte from "@sveltejs/vite-plugin-svelte" |
|||
|
|||
export default ({ mode }) => { |
|||
const isProduction = mode === "production" |
|||
return { |
|||
build: { |
|||
lib: { |
|||
entry: "src/index.js", |
|||
name: "bbui", |
|||
formats: ["es"], |
|||
}, |
|||
minify: isProduction, |
|||
}, |
|||
plugins: [svelte()], |
|||
resolve: { |
|||
dedupe: ["svelte", "svelte/internal"], |
|||
}, |
|||
rollupOptions: { |
|||
external: ["svelte", "svelte/internal"], |
|||
}, |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue