@sociallane/elements 1.0.11 → 1.0.13

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.
@@ -55,13 +55,13 @@ cd wp-content/plugins/sociallane-elements && npm install && npm run build
55
55
 
56
56
  ```bash
57
57
  # From your WordPress root (directory that contains wp-content)
58
- npx @sociallane/elements --minimal
58
+ npx @sociallane/elements --base
59
59
  ```
60
60
 
61
61
  Then add widgets by slug (no manual `widgets.json` editing):
62
62
 
63
63
  ```bash
64
- npx @sociallane/elements add hero-split faq content-block widget-filter
64
+ npx @sociallane/elements add hero-split faq-stacked content-block widget-filter
65
65
  ```
66
66
 
67
67
  Each `add` updates `widgets.json` and runs the build. You can run `add` anytime to add more widgets.
@@ -77,8 +77,8 @@ This copies the full plugin to `wp-content/plugins/sociallane-elements`, runs `n
77
77
  **Custom path:**
78
78
 
79
79
  ```bash
80
- npx @sociallane/elements --minimal path/to/wp-content/plugins/sociallane-elements
81
- npx @sociallane/elements add hero-split faq # run from WordPress root or plugin dir
80
+ npx @sociallane/elements --base path/to/wp-content/plugins/sociallane-elements
81
+ npx @sociallane/elements add --target path/to/wp-content/plugins/sociallane-elements hero-split faq-stacked
82
82
  ```
83
83
 
84
84
  **Single widget (per-widget packages):**
@@ -87,7 +87,7 @@ You can install one widget at a time via its own package. This installs the Soci
87
87
 
88
88
  ```bash
89
89
  # From your WordPress root or wp-content/plugins
90
- npx @sociallane/widget-hero-overlay
90
+ npx @sociallane/widget-content-block
91
91
  npx @sociallane/widget-faq-stacked
92
92
  ```
93
93
 
@@ -7,8 +7,11 @@ Quick reference for loading widgets via npm workspaces.
7
7
  ## One-liners
8
8
 
9
9
  ```bash
10
- # Full install REQUIRED for styled widgets (from plugin root)
11
- npm install && npm run build
10
+ # Base install (no widgets imported yet)
11
+ npx @sociallane/elements --base
12
+
13
+ # Add selected widgets
14
+ npx @sociallane/elements add hero-split faq-stacked content-block
12
15
 
13
16
  # Dev watch (rebuilds on file change)
14
17
  npm run dev
@@ -28,7 +31,7 @@ Edit `widgets.json` at plugin root:
28
31
  "hero-split",
29
32
  "hero-overlay",
30
33
  "content-block",
31
- "faq"
34
+ "faq-stacked"
32
35
  ]
33
36
  }
34
37
  ```
@@ -39,7 +42,10 @@ Only slugs listed here are loaded. Add or remove as needed.
39
42
 
40
43
  ## Widget → npm package & command
41
44
 
42
- Every widget and its npm package / add command:
45
+ Common widgets and their npm package / add command:
46
+
47
+ > Note: per-widget packages may not all be published at the same time.
48
+ > The most reliable flow is `npx @sociallane/elements --base` then `npx @sociallane/elements add ...`.
43
49
 
44
50
  | Widget | Package | Command |
45
51
  |--------|---------|---------|
@@ -52,7 +58,9 @@ Every widget and its npm package / add command:
52
58
  | cta-banner | `@sociallane/widget-cta-banner` | `npm install @sociallane/widget-cta-banner` |
53
59
  | cta-notify | `@sociallane/widget-cta-notify` | `npm install @sociallane/widget-cta-notify` |
54
60
  | cta-split | `@sociallane/widget-cta-split` | `npm install @sociallane/widget-cta-split` |
55
- | faq | `@sociallane/widget-faq` | `npm install @sociallane/widget-faq` |
61
+ | faq-stacked | `@sociallane/widget-faq-stacked` | `npm install @sociallane/widget-faq-stacked` |
62
+ | faq-split | `@sociallane/widget-faq-split` | `npm install @sociallane/widget-faq-split` |
63
+ | faq-centered | `@sociallane/widget-faq-centered` | `npm install @sociallane/widget-faq-centered` |
56
64
  | feature-grid | `@sociallane/widget-feature-grid` | `npm install @sociallane/widget-feature-grid` |
57
65
  | feature-list | `@sociallane/widget-feature-list` | `npm install @sociallane/widget-feature-list` |
58
66
  | feature-list-cta | `@sociallane/widget-feature-list-cta` | `npm install @sociallane/widget-feature-list-cta` |
@@ -62,25 +70,34 @@ Every widget and its npm package / add command:
62
70
  | form-contact | `@sociallane/widget-form-contact` | `npm install @sociallane/widget-form-contact` |
63
71
  | grid-case-studies | `@sociallane/widget-grid-case-studies` | `npm install @sociallane/widget-grid-case-studies` |
64
72
  | grid-components | `@sociallane/widget-grid-components` | `npm install @sociallane/widget-grid-components` |
65
- | grid-posts | `@sociallane/widget-grid-posts` | `npm install @sociallane/widget-grid-posts` |
73
+ | posts-grid | `@sociallane/widget-posts-grid` | `npm install @sociallane/widget-posts-grid` |
74
+ | posts-grid-overlay | `@sociallane/widget-posts-grid-overlay` | `npm install @sociallane/widget-posts-grid-overlay` |
66
75
  | grid-team | `@sociallane/widget-grid-team` | `npm install @sociallane/widget-grid-team` |
67
- | grid-testimonials | `@sociallane/widget-grid-testimonials` | `npm install @sociallane/widget-grid-testimonials` |
76
+ | testimonials-grid | `@sociallane/widget-testimonials-grid` | `npm install @sociallane/widget-testimonials-grid` |
77
+ | testimonials-masonry | `@sociallane/widget-testimonials-masonry` | `npm install @sociallane/widget-testimonials-masonry` |
78
+ | testimonials-bento | `@sociallane/widget-testimonials-bento` | `npm install @sociallane/widget-testimonials-bento` |
68
79
  | hero-announcement | `@sociallane/widget-hero-announcement` | `npm install @sociallane/widget-hero-announcement` |
69
80
  | hero-centered-image | `@sociallane/widget-hero-centered-image` | `npm install @sociallane/widget-hero-centered-image` |
70
81
  | hero-collage | `@sociallane/widget-hero-collage` | `npm install @sociallane/widget-hero-collage` |
71
82
  | hero-overlay | `@sociallane/widget-hero-overlay` | `npm install @sociallane/widget-hero-overlay` |
72
- | hero-saas | `@sociallane/widget-hero-saas` | `npm install @sociallane/widget-hero-saas` |
83
+ | hero-saas-split | `@sociallane/widget-hero-saas-split` | `npm install @sociallane/widget-hero-saas-split` |
84
+ | hero-saas-stacked | `@sociallane/widget-hero-saas-stacked` | `npm install @sociallane/widget-hero-saas-stacked` |
85
+ | hero-saas-centered | `@sociallane/widget-hero-saas-centered` | `npm install @sociallane/widget-hero-saas-centered` |
73
86
  | hero-split | `@sociallane/widget-hero-split` | `npm install @sociallane/widget-hero-split` |
74
87
  | hero-stacked-image | `@sociallane/widget-hero-stacked-image` | `npm install @sociallane/widget-hero-stacked-image` |
75
88
  | intro-pattern | `@sociallane/widget-intro-pattern` | `npm install @sociallane/widget-intro-pattern` |
76
89
  | intro-text | `@sociallane/widget-intro-text` | `npm install @sociallane/widget-intro-text` |
77
- | logo-grid | `@sociallane/widget-logo-grid` | `npm install @sociallane/widget-logo-grid` |
90
+ | logo-grid-centered | `@sociallane/widget-logo-grid-centered` | `npm install @sociallane/widget-logo-grid-centered` |
91
+ | logo-grid-row | `@sociallane/widget-logo-grid-row` | `npm install @sociallane/widget-logo-grid-row` |
92
+ | logo-grid-split | `@sociallane/widget-logo-grid-split` | `npm install @sociallane/widget-logo-grid-split` |
78
93
  | nav-default | `@sociallane/widget-nav-default` | `npm install @sociallane/widget-nav-default` |
79
94
  | nav-centered | `@sociallane/widget-nav-centered` | `npm install @sociallane/widget-nav-centered` |
80
95
  | nav-minimal | `@sociallane/widget-nav-minimal` | `npm install @sociallane/widget-nav-minimal` |
81
96
  | nav-compact | `@sociallane/widget-nav-compact` | `npm install @sociallane/widget-nav-compact` |
82
97
  | nav-floating | `@sociallane/widget-nav-floating` | `npm install @sociallane/widget-nav-floating` |
83
98
  | newsletter | `@sociallane/widget-newsletter` | `npm install @sociallane/widget-newsletter` |
99
+ | newsletter-card | `@sociallane/widget-newsletter-card` | `npm install @sociallane/widget-newsletter-card` |
100
+ | newsletter-section | `@sociallane/widget-newsletter-section` | `npm install @sociallane/widget-newsletter-section` |
84
101
  | outreach-dashboard | `@sociallane/widget-outreach-dashboard` | `npm install @sociallane/widget-outreach-dashboard` |
85
102
  | pipeline-dashboard | `@sociallane/widget-pipeline-dashboard` | `npm install @sociallane/widget-pipeline-dashboard` |
86
103
  | pricing-table | `@sociallane/widget-pricing-table` | `npm install @sociallane/widget-pricing-table` |
@@ -89,7 +106,9 @@ Every widget and its npm package / add command:
89
106
  | services | `@sociallane/widget-services` | `npm install @sociallane/widget-services` |
90
107
  | simple-page-hero | `@sociallane/widget-simple-page-hero` | `npm install @sociallane/widget-simple-page-hero` |
91
108
  | social-proof | `@sociallane/widget-social-proof` | `npm install @sociallane/widget-social-proof` |
109
+ | social-proof-trust | `@sociallane/widget-social-proof-trust` | `npm install @sociallane/widget-social-proof-trust` |
92
110
  | testimonial-quote | `@sociallane/widget-testimonial-quote` | `npm install @sociallane/widget-testimonial-quote` |
111
+ | widget-filter | `@sociallane/widget-widget-filter` | `npm install @sociallane/widget-widget-filter` |
93
112
 
94
113
  Then add the slug to `widgets.json`.
95
114
 
@@ -99,12 +118,12 @@ Then add the slug to `widgets.json`.
99
118
 
100
119
  | Category | Slugs |
101
120
  |----------|-------|
102
- | Heroes | `hero-split`, `hero-overlay`, `hero-saas`, `hero-collage`, `hero-centered-image`, `hero-stacked-image`, `hero-announcement`, `simple-page-hero` |
103
- | Content | `content-block`, `intro-text`, `intro-pattern`, `faq`, `feature-list`, `feature-list-cta`, `feature-grid` |
104
- | Cards & Grids | `card-hover-reveal`, `bento-grid`, `bento-portfolio`, `grid-posts`, `grid-team`, `grid-testimonials`, `grid-case-studies`, `grid-components`, `logo-grid`, `client-logos` |
105
- | Forms & CTAs | `cta-banner`, `cta-split`, `cta-notify`, `newsletter`, `form-contact`, `pricing-table` |
121
+ | Heroes | `hero-split`, `hero-overlay`, `hero-overlay-single`, `hero-overlay-slider`, `hero-saas-split`, `hero-saas-stacked`, `hero-saas-centered`, `hero-collage`, `hero-centered-image`, `hero-stacked-image`, `hero-announcement`, `simple-page-hero`, `page-hero-left`, `page-hero-center` |
122
+ | Content | `content-block`, `intro-text`, `intro-pattern`, `faq-stacked`, `faq-split`, `faq-centered`, `feature-list`, `feature-list-cta`, `feature-grid` |
123
+ | Cards & Grids | `card-hover-reveal`, `bento-grid`, `bento-portfolio`, `posts-grid`, `posts-grid-overlay`, `grid-team`, `testimonials-grid`, `testimonials-masonry`, `testimonials-bento`, `grid-case-studies`, `grid-components`, `logo-grid-centered`, `logo-grid-row`, `logo-grid-split`, `client-logos` |
124
+ | Forms & CTAs | `cta-banner`, `cta-split`, `cta-notify`, `newsletter`, `newsletter-card`, `newsletter-section`, `form-contact`, `pricing-table` |
106
125
  | Navigation | `nav-default`, `nav-centered`, `nav-minimal`, `nav-compact`, `nav-floating`, `footer`, `footer-brand`, `footer-links-contact` |
107
- | Other | `section-stats`, `social-proof`, `services`, `blog-grid`, `outreach-dashboard`, `pipeline-dashboard`, `sales-dashboard`, `testimonial-quote` |
126
+ | Other | `section-stats`, `social-proof`, `social-proof-trust`, `services`, `blog-grid`, `outreach-dashboard`, `pipeline-dashboard`, `sales-dashboard`, `testimonial-quote`, `widget-filter` |
108
127
 
109
128
  ---
110
129
 
@@ -29,7 +29,8 @@ Edit `widgets.json` to include only the widget slugs you want. You can also let
29
29
 
30
30
  Adding or removing widget directories is reflected in `widgets.json` automatically:
31
31
 
32
- - **On `npm install`** — The postinstall script runs `sync-widgets` then `build`. Any new folder under `packages/widgets/{slug}` with `{slug}.php` is added to `widgets.json`; removed folders are removed from the list. Existing order is preserved; new slugs are appended sorted.
32
+ - **On `npm install`** — The postinstall script runs `sync-widgets` then `build` by default. Any new folder under `packages/widgets/{slug}` with `{slug}.php` is added to `widgets.json`; removed folders are removed from the list. Existing order is preserved; new slugs are appended sorted.
33
+ The installers use `SOCIALLANE_SKIP_SYNC_WIDGETS=1` for base/cherry-pick flows so `widgets.json` is not expanded unintentionally.
33
34
  - **Manual sync** — From the plugin root run:
34
35
  ```bash
35
36
  npm run sync-widgets
@@ -42,7 +43,7 @@ Adding or removing widget directories is reflected in `widgets.json` automatical
42
43
  "hero-split",
43
44
  "hero-overlay",
44
45
  "content-block",
45
- "faq"
46
+ "faq-stacked"
46
47
  ]
47
48
  }
48
49
  ```
@@ -65,10 +66,10 @@ Use submodules so each WordPress instance only has the core package and the widg
65
66
 
66
67
  # Add only the widgets you want
67
68
  git submodule add <url-to-widget-hero-split> packages/widgets/hero-split
68
- git submodule add <url-to-widget-faq> packages/widgets/faq
69
+ git submodule add <url-to-widget-faq-stacked> packages/widgets/faq-stacked
69
70
  ```
70
71
 
71
- 3. Update **widgets.json** so its `widgets` array lists only the slugs you added (e.g. `hero-split`, `faq`).
72
+ 3. Update **widgets.json** so its `widgets` array lists only the slugs you added (e.g. `hero-split`, `faq-stacked`).
72
73
 
73
74
  4. Install and build from plugin root (both required for styled widgets):
74
75
 
@@ -123,13 +124,13 @@ The plugin is published as `@sociallane/elements`. To publish:
123
124
 
124
125
  ## Per-widget npm packages
125
126
 
126
- Individual widgets are published as `@sociallane/widget-<slug>` (e.g. `@sociallane/widget-hero-overlay`). Run from your WordPress root or `wp-content/plugins`:
127
+ Individual widgets are published as `@sociallane/widget-<slug>` (for example `@sociallane/widget-faq-stacked`). Run from your WordPress root or `wp-content/plugins`:
127
128
 
128
129
  ```bash
129
- npx @sociallane/widget-hero-overlay
130
+ npx @sociallane/widget-faq-stacked
130
131
  ```
131
132
 
132
- The installer installs the base plugin (`@sociallane/elements`) with `--minimal` if it is not present, then copies the widget into the plugin, runs `sync-widgets`, and `npm install`. Use these when you want a one-command install for a single widget. To add multiple widgets, `npx @sociallane/elements add slug1 slug2` is usually simpler.
133
+ The installer installs the base plugin (`@sociallane/elements`) with `--base` if it is not present, then delegates to `npx @sociallane/elements add --target <pluginDir> <slug>`. Use these when you want a one-command install for a single widget. To add multiple widgets, `npx @sociallane/elements add --target <pluginDir> slug1 slug2` is usually simpler.
133
134
 
134
135
  ## Adding a new widget package
135
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sociallane/elements",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Elementor widgets and elements with Tailwind CSS for SocialLane. WordPress plugin.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -77,7 +77,7 @@ function prepare_card_hover_reveal_view( array $settings, string $widget_id ): a
77
77
  $aspect_map = [ '16/9' => 'aspect-video', '4/3' => 'aspect-[4/3]', '3/2' => 'aspect-[3/2]', '1/1' => 'aspect-square', '3/4' => 'aspect-[3/4]' ];
78
78
  $aspect_class = $aspect_map[ $aspect_ratio ] ?? 'aspect-video';
79
79
  $columns = sociallane_validate_option( $settings['columns'] ?? '3', [ '2', '3' ], '3' );
80
- $grid_class = sociallane_grid_cols( $columns, [ '2' => 'grid-cols-1 sm:grid-cols-2', '3' => 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3' ] );
80
+ $grid_class = sociallane_grid_cols( $columns, [ '2' => 'grid-cols-1 sm:grid-cols-2', '3' => 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3' ] );
81
81
 
82
82
  $animation = sociallane_animation_view_data( $settings, [ 'headline' => true, 'stagger' => true ] );
83
83
 
@@ -23,7 +23,12 @@ function prepare_grid_components_view( array $settings, string $widget_id ): arr
23
23
  $fields = sociallane_extract_fields( $settings, [ 'headline' => [] ] );
24
24
  $headline_tag = sociallane_validate_heading_tag( $settings, 'headline_tag', 'h2' );
25
25
  $columns = sociallane_validate_option( $settings['columns'] ?? '4', [ '2', '3', '4' ], '4' );
26
- $grid_class = sociallane_grid_cols( $columns );
26
+ $grid_class = sociallane_grid_cols(
27
+ $columns,
28
+ [
29
+ '3' => 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
30
+ ]
31
+ );
27
32
  $placeholder_src = \Elementor\Utils::get_placeholder_image_src();
28
33
 
29
34
  $components = sociallane_process_repeater( $settings['components'] ?? [], function ( $item ) use ( $placeholder_src ) {
@@ -484,8 +484,8 @@ function prepare_widget_filter_view( array $settings, string $widget_id ): array
484
484
  // Grid columns class
485
485
  $grid_columns = [
486
486
  '2' => 'grid-cols-1 sm:grid-cols-2',
487
- '3' => 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
488
- '4' => 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
487
+ '3' => 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
488
+ '4' => 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4',
489
489
  ];
490
490
  $grid_class = $grid_columns[ $columns ] ?? $grid_columns['3'];
491
491
 
@@ -2,23 +2,60 @@
2
2
  /**
3
3
  * SocialLane Elements CLI
4
4
  *
5
+ * Install (base): npx @sociallane/elements --base [path]
5
6
  * Install (full): npx @sociallane/elements [path]
6
7
  * Add widgets: npx @sociallane/elements add <slug> [slug...]
7
8
  *
8
9
  * Install: copies package to wp-content/plugins/sociallane-elements (or path), runs npm install.
10
+ * Base install: installs core/deps and keeps widgets.json empty (no widgets loaded by default).
9
11
  * Add: copies selected widget folders into an existing plugin, updates widgets.json, runs npm install.
10
12
  */
11
13
 
12
- import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'fs';
14
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
13
15
  import { spawnSync } from 'child_process';
14
16
  import path from 'path';
15
17
  import { fileURLToPath } from 'url';
16
18
 
17
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
20
  const packageRoot = path.resolve(__dirname, '..');
19
- const widgetsSource = path.join(packageRoot, 'packages', 'widgets');
21
+ const packageJsonPath = path.join(packageRoot, 'package.json');
22
+
23
+ function getCliVersion() {
24
+ try {
25
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
26
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
27
+ } catch {
28
+ return '0.0.0';
29
+ }
30
+ }
31
+
32
+ function printHelp() {
33
+ console.log('SocialLane Elements CLI');
34
+ console.log('');
35
+ console.log('Usage:');
36
+ console.log(' npx @sociallane/elements [--base|--minimal] [target]');
37
+ console.log(' npx @sociallane/elements add [--only] [--target <path>] <slug> [slug...]');
38
+ console.log(' npx @sociallane/elements --help');
39
+ console.log(' npx @sociallane/elements --version');
40
+ console.log('');
41
+ console.log('Options:');
42
+ console.log(' --base, --minimal Install core/deps only, keep widgets.json empty');
43
+ console.log(' --only With add: replace widgets.json with exactly provided slugs');
44
+ console.log(' --target <path> With add: explicit plugin directory target');
45
+ console.log(' --help Show this help text');
46
+ console.log(' --version Show CLI version');
47
+ console.log('');
48
+ console.log('Examples:');
49
+ console.log(' npx @sociallane/elements --base');
50
+ console.log(' npx @sociallane/elements add faq-stacked hero-split');
51
+ console.log(' npx @sociallane/elements add --only --target wp-content/plugins/sociallane-elements content-block');
52
+ }
53
+
54
+ function resolvePluginDir(targetPath = '') {
55
+ if (targetPath) {
56
+ return path.resolve(process.cwd(), targetPath);
57
+ }
20
58
 
21
- function resolvePluginDir() {
22
59
  const cwd = process.cwd();
23
60
  const normalized = path.normalize(cwd);
24
61
  const pluginsSegment = 'wp-content' + path.sep + 'plugins';
@@ -41,9 +78,59 @@ function resolvePluginDir() {
41
78
  return path.join(cwd, 'wp-content', 'plugins', 'sociallane-elements');
42
79
  }
43
80
 
44
- function addWidgets(slugs) {
45
- const pluginDir = resolvePluginDir();
81
+ function getWidgetsSourceDir() {
82
+ const candidates = [
83
+ path.join(packageRoot, 'packages', 'widgets'),
84
+ path.join(packageRoot, 'widgets'),
85
+ ];
86
+
87
+ for (const dir of candidates) {
88
+ if (!existsSync(dir)) {
89
+ continue;
90
+ }
91
+ const available = readdirSync(dir, { withFileTypes: true })
92
+ .filter((d) => d.isDirectory() && existsSync(path.join(dir, d.name, `${d.name}.php`)))
93
+ .map((d) => d.name);
94
+ if (available.length > 0) {
95
+ return dir;
96
+ }
97
+ }
98
+ return candidates[0];
99
+ }
100
+
101
+ function updateWidgetsJson(pluginDir, slugsToAdd, replaceAll = false) {
102
+ const widgetsJsonPath = path.join(pluginDir, 'widgets.json');
103
+ let existing = [];
104
+
105
+ if (!replaceAll && existsSync(widgetsJsonPath)) {
106
+ try {
107
+ const raw = readFileSync(widgetsJsonPath, 'utf8');
108
+ const parsed = JSON.parse(raw);
109
+ if (Array.isArray(parsed?.widgets)) {
110
+ existing = parsed.widgets.filter((slug) => typeof slug === 'string');
111
+ }
112
+ } catch (err) {
113
+ console.warn('Could not parse widgets.json, recreating it:', err.message);
114
+ }
115
+ }
116
+
117
+ const merged = [...existing];
118
+ const existingSet = new Set(existing);
119
+ for (const slug of slugsToAdd) {
120
+ if (!existingSet.has(slug)) {
121
+ merged.push(slug);
122
+ existingSet.add(slug);
123
+ }
124
+ }
125
+
126
+ writeFileSync(widgetsJsonPath, JSON.stringify({ widgets: merged }, null, 2) + '\n');
127
+ }
128
+
129
+ function addWidgets(slugs, options = {}) {
130
+ const replaceAll = options.replaceAll === true;
131
+ const pluginDir = resolvePluginDir(options.targetPath || '');
46
132
  const widgetsDest = path.join(pluginDir, 'packages', 'widgets');
133
+ const widgetsSource = getWidgetsSourceDir();
47
134
 
48
135
  if (!existsSync(path.join(pluginDir, 'sociallane-elements.php'))) {
49
136
  console.error('Plugin not found at', pluginDir);
@@ -56,8 +143,10 @@ function addWidgets(slugs) {
56
143
  process.exit(1);
57
144
  }
58
145
 
146
+ console.log('Target plugin:', pluginDir);
147
+
59
148
  const available = readdirSync(widgetsSource, { withFileTypes: true })
60
- .filter((d) => d.isDirectory())
149
+ .filter((d) => d.isDirectory() && existsSync(path.join(widgetsSource, d.name, `${d.name}.php`)))
61
150
  .map((d) => d.name);
62
151
 
63
152
  const missing = slugs.filter((s) => !available.includes(s));
@@ -84,17 +173,16 @@ function addWidgets(slugs) {
84
173
  console.log('Added:', slug);
85
174
  }
86
175
 
87
- console.log('Updating widgets.json and building...');
88
- const result = spawnSync('node', [path.join(pluginDir, 'scripts', 'sync-widgets.js')], {
89
- cwd: pluginDir,
90
- stdio: 'inherit',
91
- });
92
- if (result.status !== 0) {
93
- process.exit(result.status || 1);
94
- }
176
+ updateWidgetsJson(pluginDir, slugs, replaceAll);
177
+ console.log('Updated widgets.json and installing dependencies...');
178
+
95
179
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
96
180
  const installResult = spawnSync(npmCmd, ['install'], {
97
181
  cwd: pluginDir,
182
+ env: {
183
+ ...process.env,
184
+ SOCIALLANE_SKIP_SYNC_WIDGETS: '1',
185
+ },
98
186
  stdio: 'inherit',
99
187
  shell: true,
100
188
  });
@@ -108,6 +196,14 @@ function addWidgets(slugs) {
108
196
  console.log('Done. Widgets added:', slugs.join(', '));
109
197
  }
110
198
 
199
+ function prepareBaseMode(pluginDir) {
200
+ const widgetsDest = path.join(pluginDir, 'packages', 'widgets');
201
+ if (!existsSync(widgetsDest)) {
202
+ mkdirSync(widgetsDest, { recursive: true });
203
+ }
204
+ updateWidgetsJson(pluginDir, [], true);
205
+ }
206
+
111
207
  function copyPackage(toDir, filter) {
112
208
  if (!existsSync(toDir)) {
113
209
  mkdirSync(toDir, { recursive: true });
@@ -154,12 +250,21 @@ function install(targetPath, minimal) {
154
250
  const targetResolved = path.resolve(target);
155
251
  const isSelf = targetResolved === path.resolve(packageRoot) || targetResolved.startsWith(packageRoot + path.sep);
156
252
 
157
- console.log('SocialLane Elements installer' + (minimal ? ' (minimal – add widgets with: npx @sociallane/elements add <slug> ...)' : ''));
253
+ console.log(
254
+ 'SocialLane Elements installer' +
255
+ (minimal ? ' (base mode – add widgets with: npx @sociallane/elements add <slug> ...)' : '')
256
+ );
158
257
  console.log('Target:', target);
159
258
 
160
259
  if (isSelf) {
260
+ if (minimal) {
261
+ prepareBaseMode(packageRoot);
262
+ }
161
263
  console.log('Target is the current package. Running npm install here...');
162
264
  } else if (existsSync(target) && existsSync(path.join(target, 'package.json'))) {
265
+ if (minimal) {
266
+ prepareBaseMode(target);
267
+ }
163
268
  console.log('Target already exists. Running npm install only...');
164
269
  } else {
165
270
  console.log('Copying package...');
@@ -168,16 +273,18 @@ function install(targetPath, minimal) {
168
273
  const rel = path.relative(packageRoot, src).split(path.sep).join('/');
169
274
  // Exclude node_modules within the package (not in parent path)
170
275
  if (rel.includes('node_modules')) return false;
171
- // Exclude packages/widgets for minimal install
276
+ // Base install should not ship widget sources; users add explicitly via `add`.
172
277
  if (rel === 'packages/widgets' || rel.startsWith('packages/widgets/')) return false;
278
+ // Exclude legacy widgets directory and generated npm-widget package folders.
279
+ if (rel === 'widgets' || rel.startsWith('widgets/')) return false;
280
+ if (rel === 'packages/npm-widgets' || rel.startsWith('packages/npm-widgets/')) return false;
173
281
  return true;
174
282
  });
175
283
  const widgetsDest = path.join(target, 'packages', 'widgets');
176
284
  if (!existsSync(widgetsDest)) {
177
285
  mkdirSync(widgetsDest, { recursive: true });
178
286
  }
179
- const widgetsJson = path.join(target, 'widgets.json');
180
- writeFileSync(widgetsJson, JSON.stringify({ widgets: [] }, null, 2));
287
+ updateWidgetsJson(target, [], true);
181
288
  } else {
182
289
  copyPackage(target);
183
290
  }
@@ -186,8 +293,15 @@ function install(targetPath, minimal) {
186
293
  const installDir = isSelf ? packageRoot : target;
187
294
  console.log('Running npm install in', installDir + '...');
188
295
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
296
+ const installEnv = {
297
+ ...process.env,
298
+ };
299
+ if (minimal) {
300
+ installEnv.SOCIALLANE_SKIP_SYNC_WIDGETS = '1';
301
+ }
189
302
  const result = spawnSync(npmCmd, ['install'], {
190
303
  cwd: installDir,
304
+ env: installEnv,
191
305
  stdio: 'inherit',
192
306
  shell: true,
193
307
  });
@@ -209,19 +323,72 @@ function install(targetPath, minimal) {
209
323
 
210
324
  function main() {
211
325
  const args = process.argv.slice(2);
212
- const minimal = args.includes('--minimal');
213
- const installArgs = args.filter((a) => a !== '--minimal');
214
326
 
215
- if (installArgs[0] === 'add') {
216
- const slugs = installArgs.slice(1).filter(Boolean);
327
+ if (args.includes('--help') || args.includes('-h')) {
328
+ printHelp();
329
+ return;
330
+ }
331
+
332
+ if (args.includes('--version') || args.includes('-v')) {
333
+ console.log(getCliVersion());
334
+ return;
335
+ }
336
+
337
+ if (args[0] === 'add') {
338
+ const addArgs = args.slice(1).filter(Boolean);
339
+ const knownAddFlags = new Set(['--only', '--target']);
340
+ let targetPath = '';
341
+ const slugs = [];
342
+ for (let i = 0; i < addArgs.length; i++) {
343
+ const arg = addArgs[i];
344
+ if (arg === '--target') {
345
+ const next = addArgs[i + 1];
346
+ if (!next || next.startsWith('-')) {
347
+ console.error('Missing value for --target');
348
+ process.exit(1);
349
+ }
350
+ targetPath = next;
351
+ i++;
352
+ continue;
353
+ }
354
+ if (arg.startsWith('-')) {
355
+ if (!knownAddFlags.has(arg)) {
356
+ console.error('Unknown option for add:', arg);
357
+ process.exit(1);
358
+ }
359
+ continue;
360
+ }
361
+ slugs.push(arg);
362
+ }
363
+
364
+ const replaceAll = addArgs.includes('--only');
217
365
  if (slugs.length === 0) {
218
- console.error('Usage: npx @sociallane/elements add <slug> [slug...]');
219
- console.error('Example: npx @sociallane/elements add hero-split faq content-block');
366
+ console.error('Usage: npx @sociallane/elements add [--only] <slug> [slug...]');
367
+ console.error('Example: npx @sociallane/elements add --only hero-split faq-stacked content-block');
220
368
  process.exit(1);
221
369
  }
222
- addWidgets(slugs);
370
+ addWidgets(slugs, { replaceAll, targetPath });
223
371
  } else {
224
- install(installArgs[0], minimal);
372
+ let minimal = false;
373
+ const positionals = [];
374
+ for (const arg of args) {
375
+ if (arg === '--minimal' || arg === '--base') {
376
+ minimal = true;
377
+ continue;
378
+ }
379
+ if (arg.startsWith('-')) {
380
+ console.error('Unknown option:', arg);
381
+ process.exit(1);
382
+ }
383
+ positionals.push(arg);
384
+ }
385
+
386
+ if (positionals.length > 1) {
387
+ console.error('Too many positional arguments. Expected at most one target path.');
388
+ process.exit(1);
389
+ }
390
+
391
+ install(positionals[0], minimal);
225
392
  }
226
393
  }
227
394
 
@@ -54,14 +54,17 @@ function ensureLocalCliShim() {
54
54
 
55
55
  ensureLocalCliShim();
56
56
 
57
- // Run sync-widgets
58
- console.log('Running sync-widgets...');
59
- const syncResult = spawnSync('node', [path.join(__dirname, 'sync-widgets.js')], {
60
- cwd: pluginRoot,
61
- stdio: 'inherit',
62
- });
63
- if (syncResult.status !== 0) {
64
- process.exit(syncResult.status || 1);
57
+ const skipSyncWidgets = process.env.SOCIALLANE_SKIP_SYNC_WIDGETS === '1';
58
+ if (!skipSyncWidgets) {
59
+ // Run sync-widgets
60
+ console.log('Running sync-widgets...');
61
+ const syncResult = spawnSync('node', [path.join(__dirname, 'sync-widgets.js')], {
62
+ cwd: pluginRoot,
63
+ stdio: 'inherit',
64
+ });
65
+ if (syncResult.status !== 0) {
66
+ process.exit(syncResult.status || 1);
67
+ }
65
68
  }
66
69
 
67
70
  // Run build
@@ -1,18 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Per-widget installer: run from a @sociallane/widget-<slug> package (npx).
4
- * Ensures SocialLane Elements plugin is installed, then copies this package's widget/
5
- * into the plugin's packages/widgets/<slug>, runs sync-widgets and npm install.
4
+ * Ensures SocialLane Elements plugin is installed in base mode, then delegates
5
+ * widget import to the main CLI add command.
6
6
  */
7
7
 
8
- import { cpSync, existsSync, mkdirSync, readFileSync } from 'fs';
8
+ import { existsSync, readFileSync } from 'fs';
9
9
  import { spawnSync } from 'child_process';
10
10
  import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
 
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const packageRoot = path.resolve(__dirname);
15
- const widgetDir = path.join(packageRoot, 'widget');
16
15
 
17
16
  function getSlugFromPackage() {
18
17
  const pkgPath = path.join(packageRoot, 'package.json');
@@ -56,62 +55,29 @@ function resolvePluginDir() {
56
55
  return path.join(cwd, 'wp-content', 'plugins', 'sociallane-elements');
57
56
  }
58
57
 
59
- function main() {
60
- const slug = getSlugFromPackage();
61
- if (!existsSync(widgetDir)) {
62
- console.error('Widget folder not found:', widgetDir);
63
- process.exit(1);
64
- }
65
- if (!existsSync(path.join(widgetDir, `${slug}.php`))) {
66
- console.error('Invalid widget: missing', path.join(widgetDir, `${slug}.php`));
67
- process.exit(1);
58
+ function runNpx(args, cwd) {
59
+ const r = spawnSync('npx', ['--yes', ...args], {
60
+ cwd,
61
+ stdio: 'inherit',
62
+ shell: true,
63
+ });
64
+ if (r.status !== 0) {
65
+ process.exit(r.status ?? 1);
68
66
  }
67
+ }
69
68
 
69
+ function main() {
70
+ const slug = getSlugFromPackage();
70
71
  const pluginDir = resolvePluginDir();
71
- const widgetsDest = path.join(pluginDir, 'packages', 'widgets');
72
72
  const pluginExists = existsSync(path.join(pluginDir, 'sociallane-elements.php'));
73
73
 
74
74
  if (!pluginExists) {
75
75
  console.log('SocialLane Elements not found. Installing base plugin...');
76
- const r = spawnSync('npx', ['--yes', '@sociallane/elements', '--minimal'], {
77
- cwd: path.dirname(pluginDir),
78
- stdio: 'inherit',
79
- shell: true,
80
- });
81
- if (r.status !== 0) {
82
- process.exit(r.status ?? 1);
83
- }
76
+ runNpx(['@sociallane/elements', '--base', pluginDir], path.dirname(pluginDir));
84
77
  }
85
78
 
86
- if (!existsSync(widgetsDest)) {
87
- mkdirSync(widgetsDest, { recursive: true });
88
- }
89
-
90
- const dest = path.join(widgetsDest, slug);
91
- cpSync(widgetDir, dest, { recursive: true, force: true });
92
- console.log('Added widget:', slug);
93
-
94
- console.log('Updating widgets.json and building...');
95
- const syncResult = spawnSync('node', [path.join(pluginDir, 'scripts', 'sync-widgets.js')], {
96
- cwd: pluginDir,
97
- stdio: 'inherit',
98
- });
99
- if (syncResult.status !== 0) {
100
- process.exit(syncResult.status ?? 1);
101
- }
102
-
103
- const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
104
- const installResult = spawnSync(npmCmd, ['install'], {
105
- cwd: pluginDir,
106
- stdio: 'inherit',
107
- shell: true,
108
- });
109
- if (installResult.status !== 0) {
110
- if (installResult.error) {
111
- console.error('Failed to run npm:', installResult.error.message);
112
- }
113
- process.exit(installResult.status ?? 1);
114
- }
79
+ console.log('Adding widget:', slug);
80
+ runNpx(['@sociallane/elements', 'add', '--target', pluginDir, slug], path.dirname(pluginDir));
115
81
 
116
82
  console.log('');
117
83
  console.log('Done. Widget', slug, 'installed. Activate the plugin in WordPress → Plugins if needed.');
@@ -108,7 +108,7 @@ function prepare_card_hover_reveal_view( array $settings, string $widget_id ): a
108
108
  $columns = in_array( $settings['columns'] ?? '3', [ '2', '3' ], true ) ? $settings['columns'] : '3';
109
109
  $grid_class = $columns === '2'
110
110
  ? 'grid-cols-1 sm:grid-cols-2'
111
- : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3';
111
+ : 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3';
112
112
 
113
113
  $animation = sociallane_animation_view_data( $settings, [ 'headline' => true, 'stagger' => true ] );
114
114
 
@@ -30,7 +30,7 @@ function prepare_grid_components_view( array $settings, string $widget_id ): arr
30
30
  $columns = in_array( $settings['columns'] ?? '4', [ '2', '3', '4' ], true ) ? $settings['columns'] : '4';
31
31
  $grid_class = [
32
32
  '2' => 'grid-cols-1 sm:grid-cols-2',
33
- '3' => 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
33
+ '3' => 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
34
34
  '4' => 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
35
35
  ][ $columns ];
36
36