diff --git a/etaf-ui-basic.el b/etaf-ui-basic.el index a0feae1..cf047a5 100644 --- a/etaf-ui-basic.el +++ b/etaf-ui-basic.el @@ -52,8 +52,7 @@ :color (plist-get theme color-key) :font-weight (when (memq variant '(strong heading)) 'bold)))) -;;;###autoload -(etaf-define-component etaf-label (&key text variant) +(etaf-ui--define-component etaf-label (&key text variant) "Render TEXT as a semantic Label." :view (text @@ -99,8 +98,7 @@ (funcall callback))))) state)) -;;;###autoload -(etaf-define-component etaf-button +(etaf-ui--define-component etaf-button (&key label on-press disabled ref class color bgcolor border padding font-weight tab-index aria-label use variant) "Render a standard pressable Button with retained callback identity. @@ -154,8 +152,7 @@ across parent Component rerenders." :bgcolor (plist-get theme bg) :border (etaf-ui--style-border (plist-get theme border-key))))) -;;;###autoload -(etaf-define-component etaf-checkbox +(etaf-ui--define-component etaf-checkbox (&key checked label on-change disabled) "Render a controlled Checkbox whose next value is sent to ON-CHANGE." :view @@ -187,8 +184,7 @@ across parent Component rerenders." ("&.enabled" :padding (0 1)) (".etaf-checkbox-mark" :font-weight bold :width 1))) -;;;###autoload -(etaf-define-component etaf-panel (&key title variant) +(etaf-ui--define-component etaf-panel (&key title variant) "Render a titled Panel with named header and default slots." :render (let ((theme (etaf-ui--style-tokens @@ -216,8 +212,7 @@ across parent Component rerenders." ("&" :padding (1 2)) (".etaf-panel-title" :font-weight bold))) -;;;###autoload -(etaf-define-component etaf-number-input +(etaf-ui--define-component etaf-number-input (&key value label on-change disabled min max) "Render a controlled minibuffer-backed NumberInput using Button." :render diff --git a/etaf-ui-data.el b/etaf-ui-data.el index 2e8cd90..024b78a 100644 --- a/etaf-ui-data.el +++ b/etaf-ui-data.el @@ -240,7 +240,7 @@ LOADING-LABEL, ERROR-LABEL, and EMPTY-LABEL override the state text." (when (eq kind 'error) "etaf-data-grid-error") (and theme (plist-get theme :ui-data-grid-error-fg))))) -(etaf-define-component etaf-ui--data-grid-body-item +(etaf-ui--define-component etaf-ui--data-grid-body-item (&key controller entry columns row-ref on-row-press row-selected-p grid-state) "Render one retained keyed DataGrid ENTRY with a cached row action." @@ -252,8 +252,7 @@ LOADING-LABEL, ERROR-LABEL, and EMPTY-LABEL override the state text." (etaf-ui--style-tokens :ui-table-border :ui-table-selected-fg :ui-table-selected-bg)))) -;;;###autoload -(etaf-define-component etaf-data-grid +(etaf-ui--define-component etaf-data-grid (&key controller columns row-key on-row-press row-ref row-selected-p loading-label error-label empty-label) "Render DATA CONTROLLER state through the public Component DSL. @@ -297,8 +296,7 @@ actions; failed candidates cannot mutate committed handler inputs." :grid-state (etaf-state))) (slot :name 'footer))) -;;;###autoload -(etaf-define-component etaf-pagination +(etaf-ui--define-component etaf-pagination (&key controller previous-ref next-ref class color bgcolor border padding aria-label) "Render a controlled pager for DATA CONTROLLER with retained controls." diff --git a/etaf-ui-style.el b/etaf-ui-style.el index 9cf7b93..b6b22ba 100644 --- a/etaf-ui-style.el +++ b/etaf-ui-style.el @@ -12,6 +12,29 @@ (require 'cl-lib) (require 'etaf) +(defvar etaf-ui--component-definition-signatures + (make-hash-table :test #'eq) + "Exact catalog definitions already installed in the current process.") + +(defmacro etaf-ui--define-component (name arguments &rest clauses) + "Define catalog Component NAME once for exact ARGUMENTS and CLAUSES. + +`package.el' may reload dependency files after byte compilation. Identical +catalog definitions are idempotent across that reload, while a changed +definition still reaches `etaf-define-component' and therefore requires the +public `etaf-component-redefine-run' boundary." + (declare (indent 2) (debug defun)) + (let ((signature + (secure-hash 'sha256 + (prin1-to-string (list name arguments clauses))))) + `(unless (equal + (gethash ',name etaf-ui--component-definition-signatures) + ,signature) + (prog1 + (etaf-define-component ,name ,arguments ,@clauses) + (puthash ',name ,signature + etaf-ui--component-definition-signatures))))) + (defconst etaf-ui--style-palette '(:ui-fg "#252A2E" :ui-bg "#FFFDF8" diff --git a/etaf-ui-table.el b/etaf-ui-table.el index 3a78d49..f380a3d 100644 --- a/etaf-ui-table.el +++ b/etaf-ui-table.el @@ -202,7 +202,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction." (lambda () (funcall callback row-value)))) (etaf-ui--table-row-children row columns)))) -(etaf-define-component etaf-ui--table-header (&key columns) +(etaf-ui--define-component etaf-ui--table-header (&key columns) "Render one retained Table header." :render (etaf-ui--table-header columns) :styles @@ -210,7 +210,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction." (".etaf-table-header" :font-weight bold :padding (0 1)) (".etaf-table-header-cell" :font-weight bold))) -(etaf-define-component etaf-ui--table-row +(etaf-ui--define-component etaf-ui--table-row (&key row identity columns row-ref on-row-press row-selected-p) "Render one retained Table ROW with stable IDENTITY." :render @@ -220,8 +220,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction." (styles (".etaf-table-row" :padding (0 1)))) -;;;###autoload -(etaf-define-component etaf-table +(etaf-ui--define-component etaf-table (&key columns rows row-key row-ref on-row-press row-selected-p) "Render ordinary ROWS as a presentational Table. diff --git a/scripts/etaf-ui-m0a-inventory.el b/scripts/etaf-ui-m0a-inventory.el index c9df7bd..91cd1d8 100644 --- a/scripts/etaf-ui-m0a-inventory.el +++ b/scripts/etaf-ui-m0a-inventory.el @@ -106,7 +106,9 @@ it is not a claim that every attribute is a declared business prop.") "\\`etaf-ui-.*\\.el\\'")) (dolist (entry (etaf-ui-m0a--read-top-level-forms file)) (let ((form (plist-get entry :form))) - (when (and (eq (car-safe form) 'etaf-define-component) + (when (and (memq (car-safe form) + '(etaf-define-component + etaf-ui--define-component)) (symbolp (cadr form)) (not (string-prefix-p "etaf-ui--" (symbol-name (cadr form))))) diff --git a/tests/etaf-ui-m0b1-doc-contract-tests.el b/tests/etaf-ui-m0b1-doc-contract-tests.el index 5ca451f..b4a65e0 100644 --- a/tests/etaf-ui-m0b1-doc-contract-tests.el +++ b/tests/etaf-ui-m0b1-doc-contract-tests.el @@ -30,6 +30,24 @@ Use Chinese punctuation when CHINESE-P is non-nil." (plist-get component :observed-business-props) (if chinese-p "、" ", ")))) +(ert-deftest etaf-ui-package-autoloads-never-register-components () + "Keep package activation from executing Component registration forms." + (dolist (file (directory-files default-directory t "\\.el\\'")) + (with-temp-buffer + (insert-file-contents file) + (goto-char (point-min)) + (while (re-search-forward "^;;;###autoload[[:space:]]*$" nil t) + (forward-comment (point-max)) + (should-not (eq (car-safe (read (current-buffer))) + 'etaf-define-component)))))) + +(ert-deftest etaf-ui-package-reload-keeps-identical-catalog-definitions () + "Allow package.el to reload exact catalog files without duplicate owners." + (let ((before (etaf-ui-m0a-component-inventory))) + (dolist (file '("etaf-ui-basic.el" "etaf-ui-table.el" "etaf-ui-data.el")) + (load-file (expand-file-name file default-directory))) + (should (equal before (etaf-ui-m0a-component-inventory))))) + (ert-deftest etaf-ui-m0b1-readmes-match-runtime-component-manifest () "Keep the README contract aligned with the runtime-derived inventory." (let* ((components (etaf-ui-m0a-component-inventory))