stalefish 8.0.10 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,10 +30,10 @@ let styles = css`
30
30
  `
31
31
 
32
32
  export default ({ wrapperStyle, holdingPen, label, property, required, indeterminate, disabled, darkBackground, onchange }) => {
33
- let checkboxEl = html`<input data-gramm="false" ${disabled ? { disabled } : ''} style="${disabled ? 'cursor: not-allowed;' : ''}" class="${styles.checkbox} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" value="${holdingPen[property] === true ? 'true' : null}" ?checked=${holdingPen[property] === true} onchange=${e => {
33
+ let checkboxEl = html`<input data-gramm="false" ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed;' : ''}" class="${styles.checkbox} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" value="${holdingPen[property] === true ? 'true' : null}" ?checked=${holdingPen[property] === true} onchange=${e => {
34
34
  formField(holdingPen, property)(e)
35
35
  onchange && onchange(e)
36
- }} type="checkbox" ${required ? { required: 'required' } : ''} />`
36
+ }} type="checkbox" ?required=${required} />`
37
37
 
38
38
  checkboxEl.indeterminate = indeterminate || false
39
39
 
@@ -0,0 +1,346 @@
1
+ import { html, css, formField, fieldIsTouched, rerender } from 'halfcab'
2
+ import calendarIcon from './icons/calendarIcon.mjs'
3
+ import solidDown from './icons/solidDown.mjs'
4
+
5
+ // Styles aligned with timePicker and prior design tweaks
6
+ const styles = css`
7
+ .textfield {
8
+ padding: 10px;
9
+ border: solid 5px #c9c9c9;
10
+ transition: border 0.3s;
11
+ outline: none;
12
+ width: 100%;
13
+ font-size: 18px;
14
+ border-radius: 0;
15
+ box-shadow: none !important;
16
+ font-weight: normal;
17
+ box-sizing: border-box;
18
+ font-family: inherit;
19
+ line-height: 1.4em;
20
+ -webkit-appearance: none;
21
+ -moz-appearance: none;
22
+ appearance: none;
23
+ z-index: 20;
24
+ position: relative;
25
+ height: 55px;
26
+ background-color: #FFF;
27
+ color: #999;
28
+ }
29
+
30
+ .noType {
31
+ caret-color: transparent;
32
+ user-select: none;
33
+ }
34
+
35
+ .textfield::placeholder { color: #999; }
36
+
37
+ .label {
38
+ transition: opacity 0.75s;
39
+ border-top-right-radius: 5px;
40
+ border-top-left-radius: 5px;
41
+ user-select: none;
42
+ position: absolute;
43
+ top: -55px;
44
+ z-index: 10;
45
+ }
46
+
47
+ .icon {
48
+ position: absolute;
49
+ right: 12px;
50
+ top: 17px;
51
+ width: 20px;
52
+ height: 20px;
53
+ opacity: 0.8;
54
+ z-index: 25;
55
+ pointer-events: none;
56
+ }
57
+
58
+ .withRightIcon { padding-right: 44px; }
59
+
60
+ .clear {
61
+ cursor: pointer;
62
+ position: absolute;
63
+ color: #AAA;
64
+ font-size: 0.8em;
65
+ line-height: 0.9em;
66
+ font-weight: normal;
67
+ box-sizing: border-box;
68
+ right: 42px; /* 10px gap from calendar icon at right:12px, width 20px */
69
+ background-color: #EEE;
70
+ padding: 5px 10px;
71
+ z-index: 30;
72
+ border-radius: 3px;
73
+ top: 17px;
74
+ }
75
+
76
+ .popup { position: absolute; z-index: 9999; top: 55px; left: 0; background: #fff; border: 1px solid #c9c9c9; box-shadow: 0 2px 10px rgba(0,0,0,.15); padding: 4px; color: #999; }
77
+ .calHeader { display: flex; align-items: center; justify-content: space-between; padding: 4px 6px; }
78
+ .navBtn { background: transparent; border: none; cursor: pointer; padding: 4px; color: #ccc; }
79
+ .navBtn:focus { outline: none; }
80
+ .navBtn:focus-visible { outline: 2px solid #c9c9c9; outline-offset: 2px; }
81
+ .monthLabel { position: relative; top: -2px; font-weight: 500; }
82
+ .arrowIcon { display: inline-block; width: 13px; height: 13px; color: #c9c9c9; line-height: 0; }
83
+ .arrowIcon > svg { display: block; width: 13px; height: 13px; }
84
+ .yearWrap { display: inline-flex; align-items: center; gap: 8px; }
85
+ .yearNum { color: #787878; min-width: 3ch; text-align: right; }
86
+ .yearControls { display: inline-flex; flex-direction: column; align-items: center; gap: 0; margin-left: 2px; }
87
+ .yearBtn { background: transparent; border: none; cursor: pointer; padding: 0; line-height: 1; width: 18px; height: 14px; display: flex; align-items: center; justify-content: center; }
88
+ .yearBtn + .yearBtn { margin-top: -2px; }
89
+ .yearBtn:focus { outline: none; }
90
+ .yearBtn:focus-visible { outline: 2px solid #c9c9c9; outline-offset: 2px; }
91
+ .yearArrowIcon { display: block; width: 12px; height: 12px; color: #c9c9c9; line-height: 0; }
92
+ .yearArrowIcon > svg { display: block; width: 12px; height: 12px; }
93
+ .weekHead { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; padding: 2px 6px; font-size: 12px; opacity: 0.8; }
94
+ .grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; padding: 0 6px 6px; }
95
+ `
96
+
97
+ // Internal symbols for per-holdingPen state
98
+ const STATE_SYMBOL = Symbol.for('stalefish.date.state')
99
+ const ID_MAP_SYMBOL = Symbol.for('stalefish.date.idMap')
100
+
101
+ function pad2 (n) { return (n < 10 ? '0' : '') + n }
102
+
103
+ function parseYMD (str) {
104
+ if (!str || typeof str !== 'string') return null
105
+ const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(str)
106
+ if (!m) return null
107
+ const y = parseInt(m[1], 10)
108
+ const mo = parseInt(m[2], 10) - 1
109
+ const d = parseInt(m[3], 10)
110
+ const dt = new Date(y, mo, d)
111
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null
112
+ return dt
113
+ }
114
+
115
+ function toYMD (date) {
116
+ if (!(date instanceof Date)) return ''
117
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
118
+ }
119
+
120
+ // Lightweight moment-style formatter for display only
121
+ // Supported tokens: YYYY, YY, MMMM, MMM, MM, M, DD, D
122
+ // This avoids adding a dependency while covering common needs
123
+ const MONTH_NAMES_FULL = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
124
+ const MONTH_NAMES_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
125
+
126
+ function formatDate (date, fmt = 'YYYY-MM-DD') {
127
+ if (!(date instanceof Date)) return ''
128
+ const Y = date.getFullYear()
129
+ const M = date.getMonth() + 1
130
+ const D = date.getDate()
131
+ // Build a token map and replace via a single regex pass to prevent
132
+ // cascading replacements (e.g., replacing D inside Dec from MMM).
133
+ const tokenMap = {
134
+ YYYY: String(Y),
135
+ YY: String(Y).slice(-2),
136
+ MMMM: MONTH_NAMES_FULL[M - 1],
137
+ MMM: MONTH_NAMES_SHORT[M - 1],
138
+ MM: pad2(M),
139
+ M: String(M),
140
+ DD: pad2(D),
141
+ D: String(D)
142
+ }
143
+ // Longest tokens first in alternation to ensure correct matching
144
+ const re = /(YYYY|MMMM|MMM|YY|MM|M|DD|D)/g
145
+ return fmt.replace(re, (match) => tokenMap[match] ?? match)
146
+ }
147
+
148
+ function ensureState (holdingPen, property) {
149
+ if (!holdingPen) return {}
150
+ if (!holdingPen[STATE_SYMBOL]) {
151
+ Object.defineProperty(holdingPen, STATE_SYMBOL, { value: {}, enumerable: false })
152
+ }
153
+ const map = holdingPen[STATE_SYMBOL]
154
+ if (!map[property]) {
155
+ map[property] = { open: false, viewMonth: null, viewYear: null, tmpDate: null }
156
+ }
157
+ return map[property]
158
+ }
159
+
160
+ function ensureId (holdingPen, property, uniqueKey) {
161
+ if (!holdingPen) return `sf-date-${property || Math.random().toString(36).slice(2)}`
162
+ if (!holdingPen[ID_MAP_SYMBOL]) {
163
+ Object.defineProperty(holdingPen, ID_MAP_SYMBOL, { value: {}, enumerable: false })
164
+ }
165
+ const map = holdingPen[ID_MAP_SYMBOL]
166
+ if (!map[property]) {
167
+ map[property] = `sf-date-${property}-${Math.random().toString(36).slice(2)}`
168
+ }
169
+ return `sf-date-${uniqueKey || map[property]}`
170
+ }
171
+
172
+ function commitValue ({ holdingPen, property, onchange, valueStr }) {
173
+ const fauxE = { currentTarget: { validity: { valid: true }, value: valueStr } }
174
+ formField(holdingPen, property)(fauxE)
175
+ onchange && onchange(fauxE)
176
+ }
177
+
178
+ function getMonthMatrix (year, month) {
179
+ const first = new Date(year, month, 1)
180
+ const startDay = first.getDay() // 0-6 Sun-Sat
181
+ const daysInMonth = new Date(year, month + 1, 0).getDate()
182
+ const prevMonthDays = new Date(year, month, 0).getDate()
183
+ const cells = []
184
+ // Fill leading blanks from previous month
185
+ for (let i = 0; i < startDay; i++) {
186
+ cells.push({ d: prevMonthDays - startDay + 1 + i, other: true })
187
+ }
188
+ // Current month days
189
+ for (let d = 1; d <= daysInMonth; d++) cells.push({ d, other: false })
190
+ // Trailing blanks to fill 6 rows of 7 or to end of grid
191
+ while (cells.length % 7 !== 0) cells.push({ d: cells.length, other: true })
192
+ return cells
193
+ }
194
+
195
+ function buildCalendarUI ({ state, today, onPickDay, onPrevMonth, onNextMonth, onPrevYear, onNextYear }) {
196
+ const y = state.viewYear
197
+ const m = state.viewMonth
198
+ const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
199
+ const weekNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
200
+ const cells = getMonthMatrix(y, m)
201
+ const sel = state.tmpDate
202
+ const isToday = (d) => today.getFullYear() === y && today.getMonth() === m && today.getDate() === d
203
+ const isSelected = (d) => sel && sel.getFullYear() === y && sel.getMonth() === m && sel.getDate() === d
204
+
205
+ return html`
206
+ <div class="${styles.calHeader}">
207
+ <button type="button" class="${styles.navBtn}" onclick=${onPrevMonth} aria-label="Previous month">
208
+ <span class="${styles.arrowIcon}" style="transform: rotate(90deg);">${solidDown({ colour: '#ccc' })}</span>
209
+ </button>
210
+ <div class="${styles.monthLabel}">
211
+ <span>${monthNames[m]}</span>
212
+ <span class="${styles.yearWrap}" aria-label="Year controls">
213
+ <span class="${styles.yearNum}">${y}</span>
214
+ <span class="${styles.yearControls}">
215
+ <button type="button" class="${styles.yearBtn}" onclick=${onNextYear} aria-label="Next year">
216
+ <span class="${styles.yearArrowIcon}" style="transform: rotate(180deg);">${solidDown({ colour: '#ccc' })}</span>
217
+ </button>
218
+ <button type="button" class="${styles.yearBtn}" onclick=${onPrevYear} aria-label="Previous year">
219
+ <span class="${styles.yearArrowIcon}" style="transform: rotate(0deg);">${solidDown({ colour: '#ccc' })}</span>
220
+ </button>
221
+ </span>
222
+ </span>
223
+ </div>
224
+ <button type="button" class="${styles.navBtn}" onclick=${onNextMonth} aria-label="Next month">
225
+ <span class="${styles.arrowIcon}" style="transform: rotate(270deg);">${solidDown({ colour: '#ccc' })}</span>
226
+ </button>
227
+ </div>
228
+ <div class="${styles.weekHead}">${weekNames.map(w => html`<div style="text-align:center;">${w}</div>`)}</div>
229
+ <div class="${styles.grid}">
230
+ ${cells.map((c, idx) => {
231
+ if (c.other) return html`<div></div>`
232
+ const d = c.d
233
+ const selStyle = isSelected(d) ? 'background:#48aaf3; color:#fff;' : 'color:#999;'
234
+ const bgStyle = isSelected(d) ? '' : 'background:#fff;'
235
+ const todayStyle = isToday(d) ? 'box-shadow: inset 0 0 0 2px #c9c9c9;' : ''
236
+ return html`<button type="button" style="padding:6px; ${selStyle} ${bgStyle} ${todayStyle} border:1px solid #c9c9c9; cursor:pointer;" onclick=${() => onPickDay(d)}>${d}</button>`
237
+ })}
238
+ </div>
239
+ `
240
+ }
241
+
242
+ export default function datePicker ({
243
+ wrapperStyle = null,
244
+ holdingPen,
245
+ label,
246
+ placeholder,
247
+ property,
248
+ required,
249
+ pattern,
250
+ disabled,
251
+ disableClear = false,
252
+ onchange,
253
+ oninput,
254
+ format = 'YYYY-MM-DD',
255
+ uniqueKey
256
+ } = {}) {
257
+ const wrapperId = ensureId(holdingPen, property, uniqueKey)
258
+ const state = ensureState(holdingPen, property)
259
+
260
+ const currentStr = holdingPen && property ? holdingPen[property] : ''
261
+ const now = new Date()
262
+ if (state.viewMonth === null || state.viewYear === null) {
263
+ const baseDate = parseYMD(currentStr) || now
264
+ state.viewMonth = baseDate.getMonth()
265
+ state.viewYear = baseDate.getFullYear()
266
+ }
267
+ if (!state.tmpDate) {
268
+ state.tmpDate = parseYMD(currentStr) || new Date(now.getFullYear(), now.getMonth(), now.getDate())
269
+ }
270
+
271
+ const commit = () => {
272
+ const valueStr = state.tmpDate ? toYMD(state.tmpDate) : ''
273
+ commitValue({ holdingPen, property, onchange, valueStr })
274
+ state.open = false
275
+ rerender()
276
+ }
277
+
278
+ const clearValue = (e) => {
279
+ if (e) { e.stopPropagation(); e.preventDefault() }
280
+ commitValue({ holdingPen, property, onchange, valueStr: '' })
281
+ state.open = false
282
+ rerender()
283
+ return false
284
+ }
285
+
286
+ // No typing allowed for date input; interaction is via popup only
287
+
288
+ const open = (e) => {
289
+ if (disabled) return
290
+ e && e.stopPropagation()
291
+ // Make opening idempotent to avoid double-toggle on focus+click
292
+ if (state.open) return
293
+ state.open = true
294
+ rerender()
295
+ if (typeof window !== 'undefined' && state.open) {
296
+ const closeOnOutside = (ev) => {
297
+ const wrapperEl = document.getElementById(wrapperId)
298
+ if (!wrapperEl) return
299
+ if (!wrapperEl.contains(ev.target)) {
300
+ state.open = false
301
+ rerender()
302
+ document.removeEventListener('mousedown', closeOnOutside, true)
303
+ }
304
+ }
305
+ setTimeout(() => document.addEventListener('mousedown', closeOnOutside, true), 0)
306
+ }
307
+ }
308
+
309
+ const popup = state.open
310
+ ? html`<div class="${styles.popup}" role="dialog" aria-modal="false">
311
+ ${buildCalendarUI({
312
+ state,
313
+ today: new Date(),
314
+ onPickDay: (d) => { state.tmpDate = new Date(state.viewYear, state.viewMonth, d); commit() },
315
+ onPrevMonth: () => { if (state.viewMonth === 0) { state.viewMonth = 11; state.viewYear -= 1 } else { state.viewMonth -= 1 }; rerender() },
316
+ onNextMonth: () => { if (state.viewMonth === 11) { state.viewMonth = 0; state.viewYear += 1 } else { state.viewMonth += 1 }; rerender() },
317
+ onPrevYear: () => { state.viewYear -= 1; rerender() },
318
+ onNextYear: () => { state.viewYear += 1; rerender() }
319
+ })}
320
+ </div>`
321
+ : ''
322
+
323
+ const displayValue = (() => {
324
+ const d = parseYMD(currentStr)
325
+ if (!d) return ''
326
+ return formatDate(d, format || 'YYYY-MM-DD')
327
+ })()
328
+
329
+ return html`
330
+ <div id="${wrapperId}" class="${wrapperStyle}" style="min-height: 55px; display: inline-block; width: calc(100% - 10px); margin: 40px 5px 5px 5px;">
331
+ <div style="display: inline-block; width: 100%; text-align: left; position: relative; padding: 0;" onclick=${e => e.stopPropagation()}>
332
+ ${label ? html`<span class="${styles.label}" style="opacity: ${(holdingPen && (holdingPen[property] === 0 || holdingPen[property])) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,0.8); position: absolute; top: -36px;">${label}${required ? ' *' : ''}</span>` : ''}
333
+ ${!disableClear ? html`<div data-clear class="${styles.clear}" onclick=${clearValue}>clear</div>` : ''}
334
+ <div class="${styles.icon}">${calendarIcon({ colour: '#ccc', width: 20, height: 20 })}</div>
335
+ <input data-gramm="false" ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : 'cursor: pointer;'}" class="${styles.textfield} ${styles.noType} ${styles.withRightIcon} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" ?required=${required}
336
+ onclick=${open}
337
+ onfocus=${open}
338
+ readonly
339
+ placeholder="${(placeholder || 'Date') + (required ? ' *' : '')}"
340
+ type="text" ${pattern ? { pattern } : ''}
341
+ .value=${displayValue} data-input />
342
+ ${popup}
343
+ </div>
344
+ </div>
345
+ `
346
+ }
@@ -1,6 +1,6 @@
1
1
  import { html, css } from 'halfcab'
2
2
 
3
- let styles = css`
3
+ const styles = css`
4
4
 
5
5
  .dropdown {
6
6
  position: relative;
@@ -34,13 +34,35 @@ let styles = css`
34
34
  }
35
35
  `
36
36
 
37
- export default ({ menuItems, visible, side, width, margin, backgroundColour }) => html`
38
- <div tabindex="-1" class="${styles.dropdown}" style="position: relative; z-index: 100000; ${!visible ? 'display: none;' : ''}${side === 'right' ? 'float: right;' : ''}">
39
- <div class="${styles.dropdownContent}" style="background-color: ${backgroundColour || '#f9f9f9'}; ${side === 'right' ? 'right: 0;' : ''} width: ${width || '160px'};${margin ? `margin: ${margin};` : ''}">
40
- ${menuItems.filter(item => !item.visibleUnder || (item.visibleUnder && typeof window !== 'undefined' && window.innerWidth <= parseInt(item.visibleUnder)))
41
- .map(item => html`
42
- ${item.separator ? html`<hr class="${styles.separator}">` : item.disabled === true ? html`<div style="opacity: 0.3; pointer-events: none;">${item.text}</div>` : html`<div style="cursor: pointer;" onclick=${item.action}>${item.text}</div>`}
43
- `)}
37
+ export default ({ menuItems = [], options = [], visible = true, side, width, margin, backgroundColour }) => {
38
+ const items = Array.isArray(menuItems) ? menuItems : (Array.isArray(options) ? options : [])
39
+ const filtered = items.filter(item => {
40
+ if (!item) return false
41
+ if (!item.visibleUnder) return true
42
+ if (typeof window === 'undefined') return false
43
+ const limit = parseInt(item.visibleUnder, 10)
44
+ return Number.isFinite(limit) ? window.innerWidth <= limit : true
45
+ })
46
+
47
+ return html`
48
+ <div tabindex="-1" class="${styles.dropdown}" style="position: relative; z-index: 100000; ${!visible ? 'display: none;' : ''}${side === 'right' ? 'float: right;' : ''}">
49
+ <div class="${styles.dropdownContent}" style="background-color: ${backgroundColour || '#f9f9f9'}; ${side === 'right' ? 'right: 0;' : ''} width: ${width || '160px'};${margin ? `margin: ${margin};` : ''}">
50
+ ${filtered.map(item => {
51
+ const label = item.text != null ? item.text : (item.name != null ? item.name : '')
52
+ const disabled = item.disabled === true
53
+ const hasAction = typeof item.action === 'function'
54
+ if (item.separator) {
55
+ return html`<hr class="${styles.separator}">`
56
+ }
57
+ if (disabled) {
58
+ return html`<div style="opacity: 0.3; pointer-events: none;">${label}</div>`
59
+ }
60
+ if (hasAction) {
61
+ return html`<div style="cursor: pointer;" onclick=${item.action}>${label}</div>`
62
+ }
63
+ return html`<div>${label}</div>`
64
+ })}
65
+ </div>
44
66
  </div>
45
- </div>
46
- `
67
+ `
68
+ }
@@ -69,7 +69,7 @@ export default ({ diameter, text, action, colour, disabled, on, icon, menuItems,
69
69
  `)}</div>` : ''}
70
70
  </div>
71
71
  <button class="${styles.fab}" onclick=${e => action(e)}
72
- ${disabled ? 'disabled' : ''}
72
+ ?disabled=${disabled}
73
73
  style="width: ${diameter}; height: ${diameter};background-color: ${colour};opacity: ${on ? '1' : '0'};bottom: ${on ? '15px' : `calc(-1 * (${diameter} + 10px))`}; ${icon ? 'padding-top: 2px' : ''};">${icon || text}</button>
74
74
  </div>
75
75
  `
@@ -1,7 +1,7 @@
1
1
  import { html, css, formField, fieldIsTouched } from 'halfcab'
2
2
  import solidDown from './icons/solidDown.mjs'
3
3
 
4
- let styles = css`
4
+ const styles = css`
5
5
  .selectBox {
6
6
  -webkit-appearance: none;
7
7
  -moz-appearance: none;
@@ -63,19 +63,20 @@ let styles = css`
63
63
  `
64
64
 
65
65
  export default ({ wrapperStyle = null, holdingPen, label, property, options, required, disabled, onchange, oninput }) => {
66
- let currentOption = options.find(option => {
66
+ const currentOption = options.find(option => {
67
67
  if (typeof option === 'object') {
68
68
  return option.value === holdingPen[property]
69
69
  } else {
70
70
  return option === holdingPen[property]
71
71
  }
72
72
  })
73
+ // text color is always #999 per latest design; placeholder state no longer affects color
73
74
 
74
75
  return html`
75
- <label style="text-align: left; position: relative; display: inline-block; width: 100%;" ${wrapperStyle ? { 'class': wrapperStyle } : ''}>
76
+ <label style="text-align: left; position: relative; display: inline-block; width: 100%;" ${wrapperStyle ? { class: wrapperStyle } : ''}>
76
77
  <div class="${styles.down}">${solidDown({ colour: '#ccc' })}</div>
77
78
  <span class="${styles.label}">${label}${required ? ' *' : ''}</span>
78
- <select ${disabled ? { disabled } : ''} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}background-color: ${typeof currentOption === 'object' && currentOption.colour ? `#${currentOption.colour}` : 'white'}" class="${styles.selectBox} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" oninput=${e => { formField(holdingPen, property)(e); oninput && oninput(e) }} onchange=${e => { formField(holdingPen, property)(e); onchange && onchange(e) }} onblur=${formField(holdingPen, property)}>
79
+ <select ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}background-color: ${typeof currentOption === 'object' && currentOption.colour ? `#${currentOption.colour}` : 'white'}; color: #999;" class="${styles.selectBox} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" oninput=${e => { formField(holdingPen, property)(e); oninput && oninput(e) }} onchange=${e => { formField(holdingPen, property)(e); onchange && onchange(e) }} onblur=${formField(holdingPen, property)}>
79
80
  <option value="${required ? 'Select an option' : ''}" ?selected=${!holdingPen[property]} ?disabled=${required} : ''}>${required ? 'Select an option' : ''}</option>
80
81
  ${options.map(option => {
81
82
  let optionValue
@@ -2,6 +2,7 @@ import { html, css, formField, fieldIsTouched } from 'halfcab'
2
2
 
3
3
  let styles = css`
4
4
  .textarea {
5
+ color: #999;
5
6
  padding: 10px;
6
7
  border: solid 5px #c9c9c9;
7
8
  transition: border 0.3s;
@@ -56,13 +57,13 @@ function change ({ e, holdingPen, property, label }) {
56
57
  }
57
58
 
58
59
  export default ({ holdingPen, label, placeholder, property, required, pattern, onkeyup, autofocus, permanentTopPlaceholder = false, permanentTopLabel = false, disabled, darkBackground, onchange, height, oninput, element }) => {
59
- let input = html`<textarea data-gramm="false" style="${height ? `height: ${height}` : ''}" class="${styles.textarea} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" onkeyup=${e => onkeyup && onkeyup(e)} ${required ? { required: 'required' } : ''} onchange=${e => { change({ e, holdingPen, property, label: styles.label }); onchange && onchange(e) }} oninput=${e => { height = window.getComputedStyle(element.querySelector('textarea')).height; change({ e, holdingPen, property, label: styles.label }); oninput && oninput(e) }} onblur=${formField(holdingPen, property)} placeholder="${placeholder || ''}${required ? ' *' : ''}" ${pattern ? { pattern } : ''}>${holdingPen[property] || ''}</textarea>`
60
+ let input = html`<textarea data-gramm="false" style="${height ? `height: ${height}` : ''}" class="${styles.textarea} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''}" onkeyup=${e => onkeyup && onkeyup(e)} ?required=${required} onchange=${e => { change({ e, holdingPen, property, label: styles.label }); onchange && onchange(e) }} oninput=${e => { height = window.getComputedStyle(element.querySelector('textarea')).height; change({ e, holdingPen, property, label: styles.label }); oninput && oninput(e) }} onblur=${formField(holdingPen, property)} placeholder="${placeholder || ''}${required ? ' *' : ''}" ${pattern ? { pattern } : ''}>${holdingPen[property] || ''}</textarea>`
60
61
 
61
62
  if (autofocus) {
62
63
  input.autofocus = true
63
64
  }
64
65
 
65
66
  return html`
66
- <label ${disabled ? { disabled } : ''} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}width: 100%; text-align: left; display: inline-block;"><span class="${styles.label}" style="opacity: ${holdingPen[property] === 0 || holdingPen[property] || (permanentTopPlaceholder || permanentTopLabel) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,${darkBackground ? 1 : 0.8}); ">${label}${required ? ' *' : ''}</span>${input}</label>
67
+ <label ?disabled=${disabled} style="${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}width: 100%; text-align: left; display: inline-block;"><span class="${styles.label}" style="opacity: ${holdingPen[property] === 0 || holdingPen[property] || (permanentTopPlaceholder || permanentTopLabel) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,${darkBackground ? 1 : 0.8}); ">${label}${required ? ' *' : ''}</span>${input}</label>
67
68
  `
68
69
  }
@@ -1,6 +1,6 @@
1
1
  import { html, css, formField, fieldIsTouched } from 'halfcab'
2
2
  // language=CSS
3
- let styles = css`
3
+ const styles = css`
4
4
  .textfield {
5
5
  padding: 10px;
6
6
  border: solid 5px #c9c9c9;
@@ -19,6 +19,7 @@ let styles = css`
19
19
  appearance: none;
20
20
  z-index: 20;
21
21
  position: relative;
22
+ color: #999;
22
23
  }
23
24
 
24
25
  .label {
@@ -63,9 +64,9 @@ let styles = css`
63
64
  `
64
65
 
65
66
  function change ({ e, holdingPen, property, label }) {
66
- let ff = formField(holdingPen, property)(e)
67
- let closestLabel = e.target.closest('label')
68
- let labelEl = closestLabel.querySelector(label.selector)
67
+ const ff = formField(holdingPen, property)(e)
68
+ const closestLabel = e.target.closest('label')
69
+ const labelEl = closestLabel.querySelector(label.selector)
69
70
  if (labelEl) {
70
71
  if (holdingPen[property] === 0 || holdingPen[property]) {
71
72
  labelEl.style.opacity = 1
@@ -78,30 +79,35 @@ function change ({ e, holdingPen, property, label }) {
78
79
  }
79
80
 
80
81
  function determineType (type) {
81
- if (type.toLowerCase() === 'float' || type.toLowerCase() === 'integer') {
82
- return 'number'
83
- }
82
+ // Normalize and guard against undefined/null/non-string values
83
+ const t = (typeof type === 'string' ? type : (type != null ? String(type) : 'text'))
84
+ const tl = t.toLowerCase()
84
85
 
85
- return type || 'input'
86
+ if (tl === 'float' || tl === 'integer') return 'number'
87
+ if (tl === 'string') return 'text'
88
+ if (tl === 'input') return 'text'
89
+ return t || 'text'
86
90
  }
87
91
 
88
92
  function determineStep (type) {
89
- if (type.toLowerCase() === 'float' || type.toLowerCase() === 'number') {
90
- return '0.1'
91
- }
92
-
93
+ const tl = typeof type === 'string' ? type.toLowerCase() : ''
94
+ if (tl === 'float' || tl === 'number') return '0.1'
93
95
  return '1'
94
96
  }
95
97
 
96
98
  export default ({ highlightBorder = false, wrapperStyle = null, holdingPen, label, placeholder, property, required, pattern, type, autofocus, valueContext, permanentTopPlaceholder = false, permanentTopLabel = false, disabled, maxCharacters, maxNumber, minNumber, darkBackground, onkeyup, oninput, onchange }) => {
97
- let input = html`<input data-gramm="false" ${disabled ? { disabled } : ''} ${maxNumber ? { max: maxNumber } : ''} ${minNumber ? { min: minNumber } : ''} ${maxCharacters ? { maxlength: maxCharacters } : ''} style="${type === 'color' ? 'height: 50px; padding: 2px 3px;' : ''}${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}" class="${styles.textfield} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''} ${highlightBorder ? styles.highlight : ''}" value="${holdingPen[property] !== undefined && holdingPen[property] !== null ? holdingPen[property] : ''}" onkeyup=${e => onkeyup && onkeyup(e)} ${required ? { required: 'required' } : ''} onchange=${e => { change({ e, holdingPen, property, label: styles.label }); onchange && onchange(e) }} oninput=${e => { change({ e, holdingPen, property, label: styles.label }); oninput && oninput(e) }} onblur=${formField(holdingPen, property)} placeholder="${placeholder || ''}${required ? ' *' : ''}" type="${determineType(type)}" ${pattern ? { pattern } : ''} ${type.toLowerCase() === 'number' ? { step: determineStep(type) } : ''} />`
99
+ const normalizedType = determineType(type)
100
+ const isColor = normalizedType === 'color'
101
+ const isNumber = normalizedType === 'number'
102
+
103
+ const input = html`<input data-gramm="false" ?disabled=${disabled} ${maxNumber ? { max: maxNumber } : ''} ${minNumber ? { min: minNumber } : ''} ${maxCharacters ? { maxlength: maxCharacters } : ''} style="${isColor ? 'height: 50px; padding: 2px 3px;' : ''}${disabled ? 'cursor: not-allowed; opacity: 0.3;' : ''}" class="${styles.textfield} ${fieldIsTouched(holdingPen, property) === true ? styles.touched : ''} ${highlightBorder ? styles.highlight : ''}" value="${holdingPen[property] !== undefined && holdingPen[property] !== null ? holdingPen[property] : ''}" onkeyup=${e => onkeyup && onkeyup(e)} ?required=${required} onchange=${e => { change({ e, holdingPen, property, label: styles.label }); onchange && onchange(e) }} oninput=${e => { change({ e, holdingPen, property, label: styles.label }); oninput && oninput(e) }} onblur=${formField(holdingPen, property)} placeholder="${placeholder || ''}${required ? ' *' : ''}" type="${normalizedType}" ${pattern ? { pattern } : ''} ${isNumber ? { step: determineStep(normalizedType) } : ''} />`
98
104
 
99
105
  if (autofocus) {
100
106
  input.autofocus = true
101
107
  }
102
108
 
103
109
  return html`
104
- <div ${wrapperStyle ? { 'class': wrapperStyle } : ''} style="display: inline-block; width: calc(100% - 10px); margin: ${label ? '40' : '5'}px 5px 5px 5px;">
110
+ <div ${wrapperStyle ? { class: wrapperStyle } : ''} style="display: inline-block; width: calc(100% - 10px); margin: ${label ? '40' : '5'}px 5px 5px 5px;">
105
111
  <label style="width: 100%; text-align: left; position: relative; padding: 0;">
106
112
  ${valueContext ? html`<div class="${styles.valueContext}">${valueContext}</div>` : ''}
107
113
  ${label ? html`<span class="${styles.label}" style="opacity: ${holdingPen[property] === 0 || holdingPen[property] || (permanentTopPlaceholder || permanentTopLabel) ? 1 : 0}; font-size: 16px; font-weight: normal; color: #999; margin-left: 5px; padding: 9px; background-color: rgba(255,255,255,${darkBackground ? 1 : 0.8}); ">${label}${required ? ' *' : ''}</span>` : ''}