Improve modal layout, improve modal button spacing, use rollup for building to expose multple BBUI entrypoints

This commit is contained in:
Andrew Kingston 2021-04-15 11:50:56 +01:00
parent 4e2fceffcb
commit 10fe99346f
18 changed files with 646 additions and 5000 deletions

View File

@ -2,38 +2,35 @@
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.58.13", "version": "1.58.13",
"license": "AGPL-3.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
"exports": { "exports": {
".": { ".": {
"import": "./dist/bbui.es.js" "import": "./dist/bbui.es.js"
}, },
"./internal": {
"import": "./dist/internal.es.js"
},
"./package.json": "./package.json", "./package.json": "./package.json",
"./dist/style.css": "./dist/style.css" "./dist/style.css": "./dist/style.css"
}, },
"scripts": { "scripts": {
"dev:builder": "vite build", "dev:builder": "rollup -cw",
"build": "vite build" "build": "rollup -c"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^16.0.0", "@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.0", "@rollup/plugin-node-resolve": "^11.2.1",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"nollup": "^0.14.1", "nollup": "^0.14.1",
"postcss": "^8.2.9", "postcss": "^8.2.9",
"rollup": "^2.34.0", "rollup": "^2.45.2",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-delete": "^1.2.0",
"rollup-plugin-hot": "^0.1.1",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-postcss": "^4.0.0", "rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-svelte-hot": "^0.11.0", "rollup-plugin-svelte": "^7.1.0",
"semantic-release": "^17.0.8", "rollup-plugin-terser": "^7.0.2",
"svelte": "^3.37.0", "svelte": "^3.37.0"
"svench": "^0.0.10-7",
"vite": "^2.1.5"
}, },
"keywords": [ "keywords": [
"svelte" "svelte"
@ -50,20 +47,22 @@
"@spectrum-css/checkbox": "^3.0.1", "@spectrum-css/checkbox": "^3.0.1",
"@spectrum-css/dialog": "^3.0.1", "@spectrum-css/dialog": "^3.0.1",
"@spectrum-css/divider": "^1.0.1", "@spectrum-css/divider": "^1.0.1",
"@spectrum-css/fieldlabel": "^3.0.1",
"@spectrum-css/icon": "^3.0.1", "@spectrum-css/icon": "^3.0.1",
"@spectrum-css/label": "^2.0.9", "@spectrum-css/label": "^2.0.9",
"@spectrum-css/link": "^3.1.1", "@spectrum-css/link": "^3.1.1",
"@spectrum-css/menu": "^3.0.1", "@spectrum-css/menu": "^3.0.1",
"@spectrum-css/modal": "^3.0.1", "@spectrum-css/modal": "^3.0.1",
"@spectrum-css/picker": "^1.0.1",
"@spectrum-css/popover": "^3.0.1", "@spectrum-css/popover": "^3.0.1",
"@spectrum-css/table": "^3.0.1", "@spectrum-css/table": "^3.0.1",
"@spectrum-css/textfield": "^3.0.1",
"@spectrum-css/toast": "^3.0.1", "@spectrum-css/toast": "^3.0.1",
"@spectrum-css/underlay": "^2.0.9", "@spectrum-css/underlay": "^2.0.9",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"markdown-it": "^12.0.4", "markdown-it": "^12.0.4",
"quill": "^1.3.7", "quill": "^1.3.7",
"sirv-cli": "^0.4.6",
"svelte-flatpickr": "^2.4.0", "svelte-flatpickr": "^2.4.0",
"svelte-portal": "^1.0.0", "svelte-portal": "^1.0.0",
"turndown": "^7.0.0" "turndown": "^7.0.0"

View File

@ -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"]()

View File

@ -4,16 +4,15 @@
export let disabled = false export let disabled = false
/** @type {('S', 'M', 'L', 'XL')} Size of button */ /** @type {('S', 'M', 'L', 'XL')} Size of button */
export let size = "M"; export let size = "M"
// Types // Types
export let cta, primary, secondary, warning, overBackground; export let cta, primary, secondary, warning, overBackground
export let quiet = false export let quiet = false
export let icon = undefined;
</script>
export let icon = undefined
</script>
<button <button
class:spectrum-Button--cta={cta} class:spectrum-Button--cta={cta}
@ -26,15 +25,18 @@
{disabled} {disabled}
on:click|preventDefault> on:click|preventDefault>
{#if icon} {#if icon}
<svg class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}" focusable="false" aria-hidden="true" aria-label="{icon}"> <svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label={icon}>
<use xlink:href="#spectrum-icon-18-{icon}" /> <use xlink:href="#spectrum-icon-18-{icon}" />
</svg> </svg>
{/if} {/if}
{#if $$slots} {#if $$slots}
<span class="spectrum-Button-label"><slot /></span> <span class="spectrum-Button-label"><slot /></span>
{/if} {/if}
</button> </button>
<style> <style>
</style> </style>

View File

@ -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>

View File

@ -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>

View File

@ -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 const dispatch = createEventDispatcher()
let editMode = false const onChange = e => {
dispatch("change", e.detail)
const updateValue = e => { value = e.detail
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
} }
</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>

View File

@ -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" export let label = undefined
export let disabled = false
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
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => {
export let value = [] dispatch("change", e.detail)
export let label = undefined value = e.detail
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
}
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)
}
} }
</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">&nbsp;</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>

View File

@ -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
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
const dispatch = createEventDispatcher()
const onChange = e => {
dispatch("change", e.detail)
value = e.detail
}
</script> </script>
<div> <Field {label} {labelPosition} {disabled} {error}>
{#if label} <Select
<Label extraSmall grey forAttr={name}>{label}</Label> {error}
{/if} {disabled}
<div class="relative"> {value}
<select {options}
{name} {placeholder}
class:thin {getOptionLabel}
class:extraThin {getOptionValue}
class:secondary on:change={onChange} />
class:outline </Field>
{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 {
position: relative !important;
display: block;
}
.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>

View File

@ -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} />

View File

@ -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>

View File

@ -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} />

View File

@ -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>

View File

@ -0,0 +1,2 @@
export { default as TextField } from "./TextField.svelte"
export { default as Select } from "./Select.svelte"

View File

@ -38,12 +38,56 @@
{#if visible} {#if visible}
<Portal target=".modal-container"> <Portal target=".modal-container">
<div class="spectrum-Underlay is-open" transition:fade={{ duration: 200 }} on:click|self={hide}> <div
<div class="spectrum-Modal-wrapper"> class="spectrum-Underlay is-open"
<div class="spectrum-Modal is-open" transition:fly={{ y: 30, duration: 200 }}> transition:fade={{ duration: 200 }}
<slot /> on:click|self={hide}>
<div class="modal-wrapper" on:click|self={hide}>
<div class="modal-inner-wrapper" on:click|self={hide}>
<div
class="spectrum-Modal is-open"
transition:fly={{ y: 30, duration: 200 }}>
<slot />
</div>
</div> </div>
</div> </div>
</div> </div>
</Portal> </Portal>
{/if} {/if}
<style>
.spectrum-Underlay {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
z-index: 999;
overflow: auto;
overflow-x: hidden;
}
.modal-wrapper {
flex: 1 1 auto;
display: flex;
flex-direction: row;
-moz-box-pack: center;
justify-content: center;
align-items: flex-start;
max-height: 100%;
}
.modal-inner-wrapper {
flex: 1 1 auto;
display: flex;
flex-direction: row;
-moz-box-pack: center;
justify-content: center;
align-items: flex-start;
width: 0;
}
.spectrum-Modal {
overflow: visible;
max-height: none;
margin: 40px 0;
}
</style>

View File

@ -29,36 +29,37 @@
} }
</script> </script>
<div class="spectrum-Dialog spectrum-Dialog--{size}" role="dialog" tabindex="-1" aria-modal="true"> <div
class="spectrum-Dialog spectrum-Dialog--{size}"
role="dialog"
tabindex="-1"
aria-modal="true">
<div class="spectrum-Dialog-grid"> <div class="spectrum-Dialog-grid">
<h1 class="spectrum-Dialog-heading">{title}</h1> <h1 class="spectrum-Dialog-heading">{title}</h1>
<hr class="spectrum-Divider spectrum-Divider--sizeS spectrum-Divider--horizontal spectrum-Dialog-divider"> <hr
class="spectrum-Divider spectrum-Divider--sizeS spectrum-Divider--horizontal spectrum-Dialog-divider" />
<!-- TODO: Remove content-grid class once Layout components are in bbui --> <!-- TODO: Remove content-grid class once Layout components are in bbui -->
<section class="spectrum-Dialog-content content-grid"> <section class="spectrum-Dialog-content content-grid">
<slot /> <slot />
</section> </section>
{#if showCancelButton || showConfirmButton} {#if showCancelButton || showConfirmButton}
<div class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"> <div
<!-- <footer class="footer-content"> class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter">
<slot name="footer" /> <slot name="footer" />
</footer> --> {#if showCancelButton}
<div class="spectrum-ButtonGroup-item"> <Button group secondary on:click={hide}>{cancelText}</Button>
<slot name="footer" /> {/if}
{#if showCancelButton} {#if showConfirmButton}
<Button secondary on:click={hide}>{cancelText}</Button> <Button
{/if} group
{#if showConfirmButton}
<Button
cta cta
primary {...$$restProps}
{...$$restProps} disabled={confirmDisabled}
disabled={confirmDisabled} on:click={confirm}>
on:click={confirm}> {confirmText}
{confirmText} </Button>
</Button> {/if}
{/if}
</div>
</div> </div>
{/if} {/if}
{#if showCloseIcon} {#if showCloseIcon}
@ -69,7 +70,6 @@
</div> </div>
</div> </div>
<style> <style>
.content-grid { .content-grid {
display: grid; display: grid;
@ -78,6 +78,14 @@
color: var(--ink); color: var(--ink);
} }
.spectrum-Dialog-content {
overflow: visible;
}
.spectrum-Dialog-buttonGroup {
gap: var(--spectrum-global-dimension-static-size-200);
}
h1 { h1 {
font-weight: normal; font-weight: normal;
} }
@ -95,27 +103,4 @@
.close-icon :global(svg) { .close-icon :global(svg) {
margin-right: 0; margin-right: 0;
} }
footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-m);
}
.footer-content {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-m);
}
</style> </style>

View File

@ -8,7 +8,7 @@
export let schema = {} export let schema = {}
export let showAutoColumns = false export let showAutoColumns = false
export let rowCount = 0 export let rowCount = 0
export let quiet = true export let quiet = false
export let loading = false export let loading = false
export let allowSelectRows = true export let allowSelectRows = true
export let allowEditRows = true export let allowEditRows = true
@ -311,8 +311,6 @@
height: 100%; height: 100%;
position: relative; position: relative;
overflow: auto; overflow: auto;
border: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--spectrum-global-color-gray-400) scrollbar-color: var(--spectrum-global-color-gray-400)
var(--spectrum-alias-background-color-primary); var(--spectrum-alias-background-color-primary);
@ -421,14 +419,16 @@
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
border-bottom: none !important; border-bottom: none !important;
border-left: none !important;
border-right: none !important;
border-top: 1px solid border-top: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important; var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
} }
tr:first-child td { tr:first-child td {
border-top: none !important; border-top: none !important;
} }
tr:last-child td {
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
}
.container:not(.quiet) td.spectrum-Table-cell--divider { .container:not(.quiet) td.spectrum-Table-cell--divider {
width: 1px; width: 1px;
border-right: 1px solid border-right: 1px solid

View File

@ -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 Load Diff