forked from tsai/budibase
18 changed files with 644 additions and 4998 deletions
@ -1,140 +1,30 @@ |
|||||
import * as path from "path" |
import svelte from "rollup-plugin-svelte" |
||||
import svelte from "rollup-plugin-svelte-hot" |
|
||||
import resolve from "@rollup/plugin-node-resolve" |
import resolve from "@rollup/plugin-node-resolve" |
||||
import commonjs from "@rollup/plugin-commonjs" |
import commonjs from "@rollup/plugin-commonjs" |
||||
import json from "@rollup/plugin-json" |
import json from "@rollup/plugin-json" |
||||
import copy from "rollup-plugin-copy" |
import { terser } from "rollup-plugin-terser" |
||||
import hmr from "rollup-plugin-hot" |
|
||||
import del from "rollup-plugin-delete" |
|
||||
import postcss from "rollup-plugin-postcss" |
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 makeConfig = ({ input, name }) => ({ |
||||
const SVENCH = !!process.env.SVENCH |
input, |
||||
const HOT = WATCH |
output: { |
||||
const PRODUCTION = !WATCH |
sourcemap: true, |
||||
|
format: "esm", |
||||
const svench = Svench({ |
file: `dist/${name}.es.js`, |
||||
// 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", |
|
||||
}, |
}, |
||||
|
plugins: [ |
||||
|
resolve(), |
||||
|
commonjs(), |
||||
|
svelte({ |
||||
|
emitCss: true, |
||||
|
}), |
||||
|
postcss(), |
||||
|
terser(), |
||||
|
json(), |
||||
|
], |
||||
}) |
}) |
||||
|
|
||||
// NOTE configs are in function form to avoid instantiating plugins of the
|
export default [ |
||||
// config that is not used for nothing (in particular, the HMR plugin launches
|
makeConfig({ input: "src/index.js", name: "bbui" }), |
||||
// a dev server on startup, this is not desired when just building for prod)
|
makeConfig({ input: "src/Form/internal/index.js", name: "internal" }), |
||||
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"]() |
|
||||
|
|||||
@ -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> |
<script> |
||||
|
import Field from "./Field.svelte" |
||||
|
import TextField from "./internal/TextField.svelte" |
||||
import { createEventDispatcher } from "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 value = null |
||||
export let label = undefined |
export let label = null |
||||
export let outline = false |
export let labelPosition = "above" |
||||
export let presentation = false |
export let placeholder = null |
||||
export let thin = false |
export let type = "text" |
||||
export let extraThin = false |
|
||||
export let large = false |
|
||||
export let border = false |
|
||||
export let edit = false |
|
||||
export let disabled = false |
export let disabled = false |
||||
export let type = undefined |
export let error = null |
||||
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 |
|
||||
|
|
||||
const updateValue = e => { |
const dispatch = createEventDispatcher() |
||||
if (type === "number") { |
const onChange = e => { |
||||
const num = parseFloat(e.target.value) |
dispatch("change", e.detail) |
||||
value = isNaN(num) ? "" : num |
value = e.detail |
||||
} else { |
|
||||
value = e.target.value |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const save = () => { |
|
||||
editMode = false |
|
||||
dispatch("save", value) |
|
||||
} |
|
||||
|
|
||||
const enableEdit = () => { |
|
||||
editMode = true |
|
||||
} |
} |
||||
</script> |
</script> |
||||
|
|
||||
<div class="container"> |
<Field {label} {labelPosition} {disabled} {error}> |
||||
{#if label || edit} |
<TextField |
||||
<div class="label-container"> |
{error} |
||||
{#if label} |
{disabled} |
||||
<Label extraSmall grey forAttr={name}>{label}</Label> |
{value} |
||||
{/if} |
{placeholder} |
||||
{#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} |
|
||||
{type} |
{type} |
||||
{name} |
on:change={onChange} /> |
||||
{placeholder} /> |
</Field> |
||||
{#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> |
|
||||
|
|||||
@ -1,324 +1,33 @@ |
|||||
<script> |
<script> |
||||
import Portal from "svelte-portal" |
|
||||
import { afterUpdate } from "svelte" |
|
||||
import { createEventDispatcher } from "svelte" |
import { createEventDispatcher } from "svelte" |
||||
import { fly } from "svelte/transition" |
import Multiselect from "./internal/Multiselect.svelte" |
||||
import Label from "../Styleguide/Label.svelte" |
import Field from "./Field.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 positionDropdown from "../Actions/position_dropdown" |
export let value = null |
||||
import clickOutside from "../Actions/click_outside" |
|
||||
|
|
||||
const dispatch = createEventDispatcher() |
|
||||
|
|
||||
export let value = [] |
|
||||
export let label = undefined |
export let label = undefined |
||||
export let align = "left" |
|
||||
export let secondary = false |
|
||||
export let outline = false |
|
||||
export let disabled = false |
export let disabled = false |
||||
export let placeholder = undefined |
export let labelPosition = "above" |
||||
export let extraThin = false |
export let error = null |
||||
|
export let placeholder = null |
||||
let options = [] |
export let options = [] |
||||
let optionsVisible = false |
export let getOptionLabel = option => option |
||||
let slot |
export let getOptionValue = option => option |
||||
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 |
|
||||
} |
|
||||
|
|
||||
function add(val) { |
const dispatch = createEventDispatcher() |
||||
value = [...value, val] |
const onChange = e => { |
||||
dispatch("change", value) |
dispatch("change", e.detail) |
||||
} |
value = e.detail |
||||
|
|
||||
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) |
|
||||
} |
|
||||
} |
} |
||||
</script> |
</script> |
||||
|
|
||||
{#if label} |
<Field {label} {labelPosition} {disabled} {error}> |
||||
<Label extraSmall grey>{label}</Label> |
<Multiselect |
||||
{/if} |
{error} |
||||
<div class="multiselect" bind:this={anchor}> |
{disabled} |
||||
<div class="tokens-wrapper"> |
{value} |
||||
<div |
{options} |
||||
class="tokens" |
{placeholder} |
||||
class:outline |
{getOptionLabel} |
||||
class:disabled |
{getOptionValue} |
||||
class:secondary |
on:change={onChange} /> |
||||
class:extraThin |
</Field> |
||||
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> |
|
||||
|
|||||
@ -1,95 +1,33 @@ |
|||||
<script> |
<script> |
||||
import Icon from "../Icons/Icon.svelte" |
import Field from "./Field.svelte" |
||||
import Label from "../Styleguide/Label.svelte" |
import Select from "./internal/Select.svelte" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
export let value = "" |
export let value = null |
||||
export let name = undefined |
|
||||
export let label = undefined |
export let label = undefined |
||||
export let thin = false |
|
||||
export let extraThin = false |
|
||||
export let secondary = false |
|
||||
export let outline = false |
|
||||
export let disabled = false |
export let disabled = false |
||||
</script> |
export let labelPosition = "above" |
||||
|
export let error = null |
||||
<div> |
export let placeholder = null |
||||
{#if label} |
export let options = [] |
||||
<Label extraSmall grey forAttr={name}>{label}</Label> |
export let getOptionLabel = option => option |
||||
{/if} |
export let getOptionValue = option => option |
||||
<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); |
|
||||
} |
|
||||
|
|
||||
.relative { |
const dispatch = createEventDispatcher() |
||||
position: relative !important; |
const onChange = e => { |
||||
display: block; |
dispatch("change", e.detail) |
||||
|
value = e.detail |
||||
} |
} |
||||
|
</script> |
||||
|
|
||||
.pointer { |
<Field {label} {labelPosition} {disabled} {error}> |
||||
right: 0 !important; |
<Select |
||||
top: 0 !important; |
{error} |
||||
bottom: 0 !important; |
{disabled} |
||||
position: absolute !important; |
{value} |
||||
pointer-events: none !important; |
{options} |
||||
padding-left: 0.5rem !important; |
{placeholder} |
||||
align-items: center !important; |
{getOptionLabel} |
||||
display: flex !important; |
{getOptionValue} |
||||
color: var(--ink); |
on:change={onChange} /> |
||||
} |
</Field> |
||||
</style> |
|
||||
|
|||||
@ -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