kmaterialize 2.3.3-1.0.2 → 2.3.3-1.0.21

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.
Files changed (42) hide show
  1. package/README.md +4 -4
  2. package/components/air-datepicker/_air-datepicker.scss +137 -0
  3. package/components/air-datepicker/air-datepicker.ts +192 -0
  4. package/components/alert/_alerts.scss +130 -0
  5. package/components/alert/alert.ts +73 -0
  6. package/components/appbar/appbar.css +301 -4
  7. package/components/button/_buttons.scss +39 -1
  8. package/components/card/_cards.scss +9 -1
  9. package/components/checkbox/_checkboxes.scss +120 -229
  10. package/components/collapsible/_collapsible.scss +50 -0
  11. package/components/color-input/_color-input.scss +124 -0
  12. package/components/color-input/color-input.ts +203 -0
  13. package/components/dropdown/dropdown.ts +1 -1
  14. package/components/file-input/_file-input-enhanced.scss +61 -0
  15. package/components/file-input/file-input.ts +203 -0
  16. package/components/kanban/_kanban.scss +148 -0
  17. package/components/kanban/kanban.ts +133 -0
  18. package/components/list/_collection.scss +10 -2
  19. package/components/number-input/number-input.ts +103 -0
  20. package/components/password-input/password-input.ts +72 -0
  21. package/components/slider/_range.scss +5 -1
  22. package/components/switch/_switches.scss +5 -0
  23. package/components/textfield/_input-fields.scss +4 -4
  24. package/components/textfield/_select.scss +54 -0
  25. package/components/tom-select/_tom-select-field.scss +256 -0
  26. package/components/tom-select/tom-select-field.ts +175 -0
  27. package/components/toolbar/_toolbar.scss +324 -0
  28. package/components/toolbar/toolbar.ts +152 -0
  29. package/dist/css/materialize.colors.min.css +2 -2
  30. package/dist/css/materialize.css +10575 -711
  31. package/dist/css/materialize.min.css +2 -2
  32. package/dist/css/materialize.min.css.map +1 -1
  33. package/dist/js/materialize.cjs.js +1136 -45
  34. package/dist/js/materialize.d.ts +227 -2
  35. package/dist/js/materialize.js +1136 -45
  36. package/dist/js/materialize.min.js +2 -2
  37. package/dist/js/materialize.mjs +1128 -46
  38. package/package.json +45 -1
  39. package/sass/_colors.scss +191 -5
  40. package/sass/_global.scss +240 -1
  41. package/sass/materialize.scss +14 -1
  42. package/sass/mixins.module.scss +6 -6
@@ -0,0 +1,203 @@
1
+ import type PickrCtor from '@simonwep/pickr';
2
+ import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
3
+ import { loadPeer } from '../../src/peer-loader';
4
+
5
+ export interface ColorInputOptions extends BaseOptions {
6
+ theme: 'classic' | 'monolith' | 'nano';
7
+ opacity: boolean;
8
+ swatches: string[];
9
+ locale?: Record<string, string>;
10
+ }
11
+
12
+ // @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/Color.ts
13
+ // Built-in French translation for Pickr's UI strings, activated by
14
+ // `data-color-locale="fr-fr"` alone - no separate options object required
15
+ // from the consumer, unlike a fully custom `options.locale` override.
16
+ const FR_FR: Record<string, string> = {
17
+ 'ui:dialog': 'boîte de dialogue du sélecteur de couleur',
18
+ 'btn:toggle': 'basculer la boîte de dialogue du sélecteur de couleur',
19
+ 'btn:swatch': 'échantillon de couleur',
20
+ 'btn:last-color': 'utiliser la couleur précédente',
21
+ 'btn:save': 'Enregistrer',
22
+ 'btn:cancel': 'Annuler',
23
+ 'btn:clear': 'Effacer',
24
+ 'aria:btn:save': 'enregistrer et fermer',
25
+ 'aria:btn:cancel': 'annuler et fermer',
26
+ 'aria:btn:clear': 'effacer et fermer',
27
+ 'aria:input': 'champ de saisie de couleur',
28
+ 'aria:palette': 'zone de sélection des couleurs',
29
+ 'aria:hue': 'curseur de sélection de teinte',
30
+ 'aria:opacity': "curseur de sélection d'opacité"
31
+ };
32
+
33
+ const _defaults: ColorInputOptions = {
34
+ theme: 'classic',
35
+ opacity: true,
36
+ swatches: [
37
+ 'rgba(244, 67, 54, 1)',
38
+ 'rgba(233, 30, 99, 0.95)',
39
+ 'rgba(156, 39, 176, 0.9)',
40
+ 'rgba(103, 58, 183, 0.85)',
41
+ 'rgba(63, 81, 181, 0.8)',
42
+ 'rgba(33, 150, 243, 0.75)',
43
+ 'rgba(3, 169, 244, 0.7)',
44
+ 'rgba(0, 188, 212, 0.7)',
45
+ 'rgba(0, 150, 136, 0.75)',
46
+ 'rgba(76, 175, 80, 0.8)',
47
+ 'rgba(139, 195, 74, 0.85)',
48
+ 'rgba(205, 220, 57, 0.9)',
49
+ 'rgba(255, 235, 59, 0.95)',
50
+ 'rgba(255, 193, 7, 1)'
51
+ ]
52
+ };
53
+
54
+ // @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/Color.ts
55
+ // Enhances a native `<input type="color" data-color-picker="pickr">` with a
56
+ // full Pickr popup (palette, hue/opacity sliders, swatches, hex/rgba/hsla).
57
+ // If `data-color-picker` is absent the input is left completely untouched -
58
+ // still a plain native color swatch, matching CrazyPHP's own fallback.
59
+ //
60
+ // Pickr remains an optional peerDependency and is loaded on demand via peer-loader.
61
+ export class ColorInput extends Component<ColorInputOptions> {
62
+ declare el: HTMLInputElement;
63
+ pickr: PickrCtor | undefined;
64
+ ready: Promise<void>;
65
+ private _swatchEl: HTMLDivElement;
66
+ private _labels: HTMLLabelElement[];
67
+
68
+ constructor(el: HTMLInputElement, options: Partial<ColorInputOptions>) {
69
+ super(el, options, ColorInput);
70
+ (this.el as any).M_ColorInput = this;
71
+
72
+ this.options = {
73
+ ...ColorInput.defaults,
74
+ ...options
75
+ };
76
+
77
+ this._swatchEl = document.createElement('div');
78
+ this.el.after(this._swatchEl);
79
+ this.el.classList.add('hide');
80
+ this.el.addEventListener('change', this._handleInputChange);
81
+ this._labels = Array.from(document.querySelectorAll('label')).filter(
82
+ label => label.htmlFor === this.el.id
83
+ );
84
+ this._labels.forEach(label => label.addEventListener('click', this._handleLabelClick));
85
+
86
+ this.ready = this._setup();
87
+ }
88
+
89
+ static get defaults(): ColorInputOptions {
90
+ return _defaults;
91
+ }
92
+
93
+ static init(el: HTMLInputElement, options?: Partial<ColorInputOptions>): ColorInput;
94
+ static init(
95
+ els: InitElements<HTMLInputElement | MElement>,
96
+ options?: Partial<ColorInputOptions>
97
+ ): ColorInput[];
98
+ static init(
99
+ els: HTMLInputElement | InitElements<HTMLInputElement | MElement>,
100
+ options: Partial<ColorInputOptions> = {}
101
+ ): ColorInput | ColorInput[] {
102
+ return super.init(els, options, ColorInput);
103
+ }
104
+
105
+ static getInstance(el: HTMLInputElement): ColorInput {
106
+ return (el as any).M_ColorInput;
107
+ }
108
+
109
+ destroy() {
110
+ this.el.removeEventListener('change', this._handleInputChange);
111
+ this._labels.forEach(label => label.removeEventListener('click', this._handleLabelClick));
112
+ this.pickr?.destroyAndRemove();
113
+ this._swatchEl.remove();
114
+ this.el.classList.remove('hide');
115
+ (this.el as any).M_ColorInput = undefined;
116
+ }
117
+
118
+ _handleInputChange = () => {
119
+ this.pickr?.setColor(this.el.value);
120
+ };
121
+
122
+ _handleLabelClick = (event: MouseEvent) => {
123
+ // A label associated with a hidden input[type=color] otherwise opens
124
+ // the browser's native picker (notably in Firefox). Route the same
125
+ // accessible label activation to Pickr instead.
126
+ event.preventDefault();
127
+ void this.ready.then(() => this.pickr?.show());
128
+ };
129
+
130
+ async _setup(): Promise<void> {
131
+ const Pickr = await loadPeer<typeof PickrCtor>(
132
+ {
133
+ specifier: '@simonwep/pickr',
134
+ globalName: 'Pickr',
135
+ feature: 'Color input (Pickr) enhancement',
136
+ cdnHint: '<link rel="stylesheet" href="path/to/pickr-classic.min.css">\n' +
137
+ ' <script src="path/to/pickr.min.js"></script>\n' +
138
+ ' (self-hosted - copy from node_modules/@simonwep/pickr/dist/, or a CDN of your choice)'
139
+ },
140
+ () => import('@simonwep/pickr')
141
+ );
142
+
143
+ const dataset = this.el.dataset;
144
+ const theme = (['classic', 'monolith', 'nano'].includes(dataset.colorTheme ?? '')
145
+ ? dataset.colorTheme
146
+ : this.options.theme) as ColorInputOptions['theme'];
147
+ const lockOpacity = dataset.colorOpacity !== undefined
148
+ ? ['false', '0', '', 'null'].includes(dataset.colorOpacity)
149
+ : !this.options.opacity;
150
+
151
+ const pickrOptions: any = {
152
+ el: this._swatchEl,
153
+ theme,
154
+ lockOpacity,
155
+ swatches: this.options.swatches,
156
+ components: {
157
+ preview: true,
158
+ opacity: true,
159
+ hue: true,
160
+ interaction: {
161
+ hex: true,
162
+ rgba: true,
163
+ hsla: true,
164
+ hsva: true,
165
+ cmyk: true,
166
+ input: true,
167
+ clear: true,
168
+ save: true
169
+ }
170
+ }
171
+ };
172
+
173
+ if (this.options.locale) {
174
+ pickrOptions.i18n = this.options.locale;
175
+ } else if (dataset.colorLocale === 'fr-fr') {
176
+ pickrOptions.i18n = FR_FR;
177
+ }
178
+
179
+ const currentValue = this.el.getAttribute('value');
180
+ if (currentValue && currentValue !== 'randomHex()') {
181
+ pickrOptions.default = currentValue;
182
+ } else if (this.el.hasAttribute('default')) {
183
+ const currentDefault = this.el.getAttribute('default');
184
+ if (currentDefault === 'randomHex()') {
185
+ const randomColor = Math.floor(Math.random() * 16777216);
186
+ pickrOptions.default = `#${randomColor.toString(16).padStart(6, '0')}`;
187
+ } else if (currentDefault) {
188
+ pickrOptions.default = currentDefault;
189
+ }
190
+ }
191
+
192
+ this.pickr = Pickr.create(pickrOptions);
193
+ if (this.el.disabled) this.pickr.disable();
194
+ this.pickr.on('save', (color: any, instance: any) => {
195
+ const hexa: string = color?.toHEXA().toString();
196
+ if (hexa) {
197
+ this.el.value = hexa;
198
+ this.el.dispatchEvent(new Event('change', { bubbles: true }));
199
+ }
200
+ instance.hide();
201
+ });
202
+ }
203
+ }
@@ -248,7 +248,7 @@ export class Dropdown extends Component<DropdownOptions> implements Openable {
248
248
  if (this.options.closeOnClick && target.closest('.dropdown-content') && !this.isTouchMoving) {
249
249
  // isTouchMoving to check if scrolling on mobile.
250
250
  this.close();
251
- } else if (!target.closest('.dropdown-content')) {
251
+ } else if (!target.closest('.dropdown-content') && !target.closest('.dropdown-trigger')) {
252
252
  // Do this one frame later so that if the element clicked also triggers _handleClick
253
253
  // For example, if a label for a select was clicked, that we don't close/open the dropdown
254
254
  setTimeout(() => {
@@ -0,0 +1,61 @@
1
+ // @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/resources/Scss/style/enhancement/filepond_materializecss.scss
2
+ // FilePond's own drop-label defaults to a near-black hardcoded color that
3
+ // doesn't track kmaterialize's theme tokens - use an MD3 token instead.
4
+ // FilePond's injected root is a sibling of the original (now hidden) file
5
+ // input, which pushes the Materialize `<label>` that used to trail the
6
+ // input out of position - pin it back to the field's left edge.
7
+ .filepond--drop-label.filepond--drop-label label {
8
+ color: var(--md-sys-color-on-surface-variant);
9
+ }
10
+
11
+ .filepond--root + label {
12
+ left: 0;
13
+ }
14
+
15
+ // Once FileInput (components/file-input/file-input.ts) enhances a
16
+ // .file-field, the stock button/file-path chrome is hidden and FilePond's
17
+ // own root (moved here to be a direct child, not left buried inside the
18
+ // now-hidden .btn) is the entire upload affordance - .file-field's normal
19
+ // 2-column button+text-field grid no longer applies.
20
+ .file-field.file-field-enhanced {
21
+ display: block;
22
+
23
+ > label {
24
+ position: static;
25
+ display: block;
26
+ margin-bottom: 0.375rem;
27
+ color: var(--md-sys-color-on-surface);
28
+ transform: none;
29
+ }
30
+
31
+ .filepond--root {
32
+ margin: 0.5rem 0 1rem 0;
33
+ font-size: 1rem;
34
+ }
35
+
36
+ .filepond--panel-root {
37
+ border-radius: 12px;
38
+ background-color: var(--md-sys-color-surface-variant);
39
+ }
40
+
41
+ .filepond--drop-label {
42
+ color: var(--md-sys-color-on-surface-variant);
43
+ }
44
+
45
+ .filepond--label-action {
46
+ color: var(--md-sys-color-primary);
47
+ text-decoration-color: var(--md-sys-color-primary);
48
+ }
49
+
50
+ .filepond--drop-label .material-icons {
51
+ font-size: 1.25rem;
52
+ line-height: 1;
53
+ vertical-align: middle;
54
+ }
55
+
56
+ .filepond-label-content {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 0.5rem;
60
+ }
61
+ }
@@ -0,0 +1,203 @@
1
+ import type * as FilePondNS from 'filepond';
2
+ import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
3
+ import { loadPeer } from '../../src/peer-loader';
4
+
5
+ export type FilePondPluginName = 'image-preview' | 'file-validate-type' | 'image-exif-orientation';
6
+
7
+ export interface FileInputOptions extends BaseOptions {
8
+ /** Which optional FilePond plugins to register - each is its own optional peerDependency. */
9
+ plugins: FilePondPluginName[];
10
+ }
11
+
12
+ const _defaults: FileInputOptions = {
13
+ plugins: []
14
+ };
15
+
16
+ // Each plugin needs its own literal `import('literal-string')` call (see
17
+ // peer-loader.ts) so Vite/webpack/Rollup can statically discover it - a
18
+ // generic specifier-driven loop can't produce that, hence one entry per
19
+ // plugin instead of building the import call from PLUGIN_SPECS's string.
20
+ const PLUGIN_LOADERS: Record<
21
+ FilePondPluginName,
22
+ { specifier: string; globalName: string; importer: () => Promise<unknown> }
23
+ > = {
24
+ 'image-preview': {
25
+ specifier: 'filepond-plugin-image-preview',
26
+ globalName: 'FilePondPluginImagePreview',
27
+ importer: () => import('filepond-plugin-image-preview')
28
+ },
29
+ 'file-validate-type': {
30
+ specifier: 'filepond-plugin-file-validate-type',
31
+ globalName: 'FilePondPluginFileValidateType',
32
+ importer: () => import('filepond-plugin-file-validate-type')
33
+ },
34
+ 'image-exif-orientation': {
35
+ specifier: 'filepond-plugin-image-exif-orientation',
36
+ globalName: 'FilePondPluginImageExifOrientation',
37
+ importer: () => import('filepond-plugin-image-exif-orientation')
38
+ }
39
+ };
40
+
41
+ // @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/File.ts
42
+ // Enhances the EXISTING `.file-field` structure in place - add
43
+ // `data-file-picker="filepond"` (+ optional `data-file-plugins="a,b,c"`) to
44
+ // an otherwise completely stock `.file-field`, nothing else about the
45
+ // markup changes. FilePond.create() hides the native `<input type=file>`
46
+ // and injects its own root as a *sibling*, it never removes anything from
47
+ // the DOM, so the surrounding button/label/file-path-wrapper stay intact
48
+ // at rest - only the interactive drop area swaps in visually.
49
+ //
50
+ // FilePond and its plugins are optional peerDependencies, loaded on demand
51
+ // via peer-loader.
52
+ export class FileInput extends Component<FileInputOptions> {
53
+ pond: FilePondNS.FilePond | undefined;
54
+ ready: Promise<void>;
55
+ private _fileEl: HTMLInputElement | null;
56
+ private _pathEl: HTMLInputElement | null;
57
+
58
+ constructor(el: HTMLElement, options: Partial<FileInputOptions>) {
59
+ super(el, options, FileInput);
60
+ (this.el as any).M_FileInput = this;
61
+
62
+ const pluginsAttr = this.el.dataset.filePlugins;
63
+ this.options = {
64
+ ...FileInput.defaults,
65
+ plugins: pluginsAttr
66
+ ? (pluginsAttr.split(',').map((p) => p.trim()) as FilePondPluginName[])
67
+ : FileInput.defaults.plugins,
68
+ ...options
69
+ };
70
+
71
+ this._fileEl = this.el.querySelector<HTMLInputElement>('input[type="file"]');
72
+ this._pathEl = this.el.querySelector<HTMLInputElement>('.file-path');
73
+
74
+ this.ready = this._setup();
75
+ }
76
+
77
+ static get defaults(): FileInputOptions {
78
+ return _defaults;
79
+ }
80
+
81
+ static init(el: HTMLElement, options?: Partial<FileInputOptions>): FileInput;
82
+ static init(els: InitElements<MElement>, options?: Partial<FileInputOptions>): FileInput[];
83
+ static init(
84
+ els: HTMLElement | InitElements<MElement>,
85
+ options: Partial<FileInputOptions> = {}
86
+ ): FileInput | FileInput[] {
87
+ return super.init(els, options, FileInput);
88
+ }
89
+
90
+ static getInstance(el: HTMLElement): FileInput {
91
+ return (el as any).M_FileInput;
92
+ }
93
+
94
+ destroy() {
95
+ this.el.classList.remove('file-field-enhanced');
96
+ this.el.querySelector<HTMLElement>('.btn')?.classList.remove('hide');
97
+ this.el.querySelector<HTMLElement>('.file-path-wrapper')?.classList.remove('hide');
98
+ this.pond?.destroy();
99
+ (this.el as any).M_FileInput = undefined;
100
+ }
101
+
102
+ /** Convenience wrapper over the underlying FilePond instance's own getFiles(). */
103
+ getFiles() {
104
+ return this.pond?.getFiles() ?? [];
105
+ }
106
+
107
+ async _setup(): Promise<void> {
108
+ if (!this._fileEl) {
109
+ console.error(Error('.file-field enhanced with data-file-picker="filepond" needs an input[type=file]'));
110
+ return;
111
+ }
112
+
113
+ const FilePond = await loadPeer<typeof FilePondNS>(
114
+ {
115
+ specifier: 'filepond',
116
+ globalName: 'FilePond',
117
+ feature: 'File input (FilePond) enhancement',
118
+ cdnHint:
119
+ '<link rel="stylesheet" href="path/to/filepond.min.css">\n' +
120
+ ' <script src="path/to/filepond.min.js"></script>\n' +
121
+ ' (self-hosted - copy from node_modules/filepond/dist/, or a CDN of your choice)'
122
+ },
123
+ () => import('filepond')
124
+ );
125
+
126
+ const pluginNames = new Set(this.options.plugins);
127
+ // file-validate-type is auto-registered whenever the input restricts
128
+ // `accept`, matching CrazyPHP's own convention, even if the consumer
129
+ // didn't list it explicitly in data-file-plugins.
130
+ if (this._fileEl.accept) pluginNames.add('file-validate-type');
131
+
132
+ for (const name of pluginNames) {
133
+ const loader = PLUGIN_LOADERS[name];
134
+ if (!loader) continue;
135
+ const plugin = await loadPeer(
136
+ {
137
+ specifier: loader.specifier,
138
+ globalName: loader.globalName,
139
+ feature: `File input FilePond plugin "${name}"`,
140
+ cdnHint: `<script src="path/to/${loader.specifier}.min.js"></script> (self-hosted - copy from node_modules/${loader.specifier}/dist/, or a CDN of your choice)`
141
+ },
142
+ loader.importer
143
+ );
144
+ FilePond.registerPlugin(plugin as any);
145
+ }
146
+
147
+ const pondOptions: FilePondNS.FilePondOptions = {};
148
+ const { dataset } = this._fileEl;
149
+ const readBoolean = (value: string | undefined) => value !== 'false';
150
+
151
+ pondOptions.allowMultiple = this._fileEl.multiple;
152
+ pondOptions.disabled = this._fileEl.disabled;
153
+ if (dataset.defaultFile) pondOptions.files = [dataset.defaultFile];
154
+ if (dataset.maxFiles) pondOptions.maxFiles = Number(dataset.maxFiles);
155
+ if (this._fileEl.multiple) pondOptions.allowReorder = readBoolean(dataset.allowReorder);
156
+ if (dataset.labelIdle) pondOptions.labelIdle = dataset.labelIdle;
157
+ if (dataset.instantUpload !== undefined) pondOptions.instantUpload = readBoolean(dataset.instantUpload);
158
+ if (dataset.dropValidation !== undefined) pondOptions.dropValidation = readBoolean(dataset.dropValidation);
159
+ if (dataset.imagePreviewHeight) pondOptions.imagePreviewHeight = Number(dataset.imagePreviewHeight);
160
+ if (dataset.stylePanelLayout) {
161
+ pondOptions.stylePanelLayout = dataset.stylePanelLayout as FilePondNS.FilePondOptions['stylePanelLayout'];
162
+ }
163
+ if (dataset.stylePanelAspectRatio) pondOptions.stylePanelAspectRatio = dataset.stylePanelAspectRatio;
164
+
165
+ this.pond = FilePond.create(this._fileEl, pondOptions);
166
+
167
+ // FilePond inserts its own root as a *sibling* of the input it was
168
+ // given - but that input lives inside .btn (stock .file-field markup
169
+ // is `.btn > input[type=file]`), so the fresh FilePond root lands
170
+ // inside .btn too. Move it back out to be a direct child of .file-field
171
+ // itself before hiding .btn, or hiding .btn would take the whole
172
+ // FilePond widget down with it.
173
+ const pondRoot = this.pond.element;
174
+ if (pondRoot) {
175
+ this.el.appendChild(pondRoot);
176
+ const fileHeight = Number(dataset.fileHeight);
177
+ if (Number.isFinite(fileHeight) && fileHeight > 0) {
178
+ (pondRoot as HTMLElement).style.height = `${fileHeight}px`;
179
+ }
180
+ // Matches CrazyPHP's own File.ts behavior - FilePond's free-tier
181
+ // "Powered by FilePond" credit link doesn't fit a themed, branded UI.
182
+ pondRoot.querySelectorAll('.filepond--credits').forEach((el) => el.remove());
183
+ }
184
+
185
+ // Everything from the stock .file-field is now redundant - FilePond's
186
+ // own drop zone is the entire upload affordance. Hidden, not removed,
187
+ // so the .file-field structure this component promises to preserve is
188
+ // still there, just not shown alongside FilePond's widget.
189
+ this.el.classList.add('file-field-enhanced');
190
+ this.el.querySelector<HTMLElement>('.btn')?.classList.add('hide');
191
+ this.el.querySelector<HTMLElement>('.file-path-wrapper')?.classList.add('hide');
192
+ this._syncPathInput();
193
+ this.pond.on('updatefiles', this._syncPathInput);
194
+ }
195
+
196
+ _syncPathInput = () => {
197
+ if (!this._pathEl || !this.pond) return;
198
+ this._pathEl.value = this.pond
199
+ .getFiles()
200
+ .map((f) => f.filename)
201
+ .join(', ');
202
+ };
203
+ }
@@ -0,0 +1,148 @@
1
+ .kanban-board {
2
+ --kanban-column-width: 20rem;
3
+ --kanban-gap: 1rem;
4
+ display: flex;
5
+ align-items: flex-start;
6
+ gap: var(--kanban-gap);
7
+ width: 100%;
8
+ padding: 0.25rem;
9
+ overflow-x: auto;
10
+ box-sizing: border-box;
11
+ overscroll-behavior-x: contain;
12
+
13
+ &.kanban-wrap {
14
+ flex-wrap: wrap;
15
+ overflow-x: visible;
16
+ }
17
+ }
18
+
19
+ .kanban-column {
20
+ display: flex;
21
+ flex: 0 0 var(--kanban-column-width);
22
+ flex-direction: column;
23
+ min-height: 15rem;
24
+ padding: 0.75rem;
25
+ border: 1px solid var(--md-sys-color-outline-variant);
26
+ border-radius: 0.875rem;
27
+ background: var(--md-sys-color-surface-variant);
28
+ box-sizing: border-box;
29
+ transition: border-color 160ms ease, background-color 160ms ease;
30
+
31
+ &.is-drag-over {
32
+ border-color: var(--md-sys-color-primary);
33
+ background: color-mix(in srgb, var(--md-sys-color-primary-container) 55%, var(--md-sys-color-surface-variant));
34
+ }
35
+ }
36
+
37
+ .kanban-column-header {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: space-between;
41
+ gap: 0.5rem;
42
+ padding: 0.25rem 0.25rem 0.75rem;
43
+ color: var(--md-sys-color-on-surface-variant);
44
+ }
45
+
46
+ .kanban-column-title {
47
+ margin: 0;
48
+ font-size: 1rem;
49
+ font-weight: 600;
50
+ }
51
+
52
+ .kanban-column-count {
53
+ display: inline-flex;
54
+ min-width: 1.5rem;
55
+ min-height: 1.5rem;
56
+ align-items: center;
57
+ justify-content: center;
58
+ padding: 0 0.375rem;
59
+ border-radius: 999px;
60
+ background: var(--md-sys-color-surface);
61
+ color: var(--md-sys-color-on-surface);
62
+ font-size: 0.75rem;
63
+ }
64
+
65
+ .kanban-column-body {
66
+ display: flex;
67
+ flex: 1 1 auto;
68
+ flex-direction: column;
69
+ gap: 0.75rem;
70
+ min-height: 8rem;
71
+ }
72
+
73
+ .kanban-card {
74
+ position: relative;
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 0.625rem;
78
+ padding: 1rem;
79
+ border: 1px solid var(--md-sys-color-outline-variant);
80
+ border-radius: 0.75rem;
81
+ background: var(--md-sys-color-surface);
82
+ color: var(--md-sys-color-on-surface);
83
+ box-shadow: var(--md-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, 0.14));
84
+ cursor: grab;
85
+ user-select: none;
86
+
87
+ &:active,
88
+ &.is-dragging {
89
+ cursor: grabbing;
90
+ }
91
+
92
+ &.is-dragging {
93
+ opacity: 0.45;
94
+ }
95
+
96
+ &:focus-visible {
97
+ outline: 2px solid var(--md-sys-color-primary);
98
+ outline-offset: 2px;
99
+ }
100
+ }
101
+
102
+ .kanban-card-title {
103
+ margin: 0;
104
+ font-size: 1rem;
105
+ font-weight: 600;
106
+ }
107
+
108
+ .kanban-card-description {
109
+ margin: 0;
110
+ color: var(--md-sys-color-on-surface-variant);
111
+ font-size: 0.875rem;
112
+ }
113
+
114
+ .kanban-card-meta {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: space-between;
118
+ gap: 0.5rem;
119
+ color: var(--md-sys-color-on-surface-variant);
120
+ font-size: 0.75rem;
121
+ }
122
+
123
+ .kanban-card-label {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ align-self: flex-start;
127
+ padding: 0.1875rem 0.5rem;
128
+ border-radius: 999px;
129
+ background: var(--md-sys-color-secondary-container);
130
+ color: var(--md-sys-color-on-secondary-container);
131
+ font-size: 0.75rem;
132
+ }
133
+
134
+ .kanban-empty {
135
+ display: grid;
136
+ min-height: 5rem;
137
+ place-items: center;
138
+ border: 1px dashed var(--md-sys-color-outline);
139
+ border-radius: 0.625rem;
140
+ color: var(--md-sys-color-on-surface-variant);
141
+ font-size: 0.875rem;
142
+ }
143
+
144
+ @media (max-width: 600px) {
145
+ .kanban-board {
146
+ --kanban-column-width: min(20rem, calc(100vw - 3rem));
147
+ }
148
+ }