@sociallane/elements 1.0.7 → 1.0.9

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.
@@ -202,7 +202,9 @@ npm run build</code></pre>
202
202
  </summary>
203
203
  <div class="p-5 pt-0 border-t border-neutral-200 dark:border-neutral-700">
204
204
  <p class="mb-2"><?php esc_html_e( 'When widgets.json exists at the plugin root, the plugin runs in package mode: only widget components are loaded. The onboarding wizard, SocialLane settings page, widget preview pages, and this setup guide are not available on that installation.', 'sociallane-elements' ); ?></p>
205
- <p class="mb-2"><?php esc_html_e( 'To get the full admin (widget management, previews, this setup page), remove or rename widgets.json; the plugin will then use the Widget Manager and show all admin features.', 'sociallane-elements' ); ?></p>
205
+ <p class="mb-2"><?php esc_html_e( 'To use full admin without removing widgets.json, add this to wp-config.php:', 'sociallane-elements' ); ?></p>
206
+ <pre class="bg-neutral-100 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-600 rounded-lg p-4 text-sm overflow-x-auto text-neutral-800 dark:text-neutral-200 mb-2"><code>define( 'SOCIALLANE_ELEMENTS_FULL_ADMIN', true );</code></pre>
207
+ <p class="mb-2"><?php esc_html_e( 'Alternatively, remove or rename widgets.json; the plugin will then use the Widget Manager and show all admin features.', 'sociallane-elements' ); ?></p>
206
208
  </div>
207
209
  </details>
208
210
  </section>
@@ -121,7 +121,11 @@ npm run build</code></pre>
121
121
  <?php esc_html_e( 'When widgets.json exists at the plugin root, the plugin runs in package mode: only widget components are loaded. The onboarding wizard, SocialLane settings page, widget preview pages, and this setup guide are not available on that installation.', 'sociallane-elements' ); ?>
122
122
  </p>
123
123
  <p class="text-neutral-600 mb-2">
124
- <?php esc_html_e( 'To get the full admin (widget management, previews, this setup page), remove or rename widgets.json; the plugin will then use the Widget Manager and show all admin features.', 'sociallane-elements' ); ?>
124
+ <?php esc_html_e( 'To use full admin without removing widgets.json, add to wp-config.php:', 'sociallane-elements' ); ?>
125
+ <code class="bg-neutral-100 px-1 rounded text-sm block mt-1">define( 'SOCIALLANE_ELEMENTS_FULL_ADMIN', true );</code>
126
+ </p>
127
+ <p class="text-neutral-600 mb-2">
128
+ <?php esc_html_e( 'Alternatively, remove or rename widgets.json; the plugin will then use the Widget Manager and show all admin features.', 'sociallane-elements' ); ?>
125
129
  </p>
126
130
  </section>
127
131
 
@@ -81,6 +81,18 @@ npx @sociallane/elements --minimal path/to/wp-content/plugins/sociallane-element
81
81
  npx @sociallane/elements add hero-split faq # run from WordPress root or plugin dir
82
82
  ```
83
83
 
84
+ **Single widget (per-widget packages):**
85
+
86
+ You can install one widget at a time via its own package. This installs the SocialLane Elements plugin if needed, then adds that widget and builds:
87
+
88
+ ```bash
89
+ # From your WordPress root or wp-content/plugins
90
+ npx @sociallane/widget-hero-overlay
91
+ npx @sociallane/widget-faq-stacked
92
+ ```
93
+
94
+ Each package is published as `@sociallane/widget-<slug>`. Use this when you only need one or two widgets and want a single command per widget.
95
+
84
96
  ---
85
97
 
86
98
  ## Post-install
@@ -121,6 +121,16 @@ The plugin is published as `@sociallane/elements`. To publish:
121
121
 
122
122
  `.npmignore` excludes `node_modules`, `.git`, and dev files so the tarball contains plugin source and `packages/`. Consumers run `npm install` in the plugin directory after copying to `wp-content/plugins` to install workspaces and trigger the build.
123
123
 
124
+ ## Per-widget npm packages
125
+
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
+
128
+ ```bash
129
+ npx @sociallane/widget-hero-overlay
130
+ ```
131
+
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
+
124
134
  ## Adding a new widget package
125
135
 
126
136
  1. Add the widget directory under `packages/widgets/{slug}` with `{slug}.php`, `data/`, `templates/`, and `package.json` (see existing widgets).
@@ -147,14 +147,23 @@ class Button extends Widget_Base {
147
147
  ];
148
148
 
149
149
  $attrs = [];
150
- if ( ! empty( $url_data['is_external'] ) ) {
151
- $attrs[] = 'target="_blank"';
152
- $attrs[] = 'rel="noopener noreferrer"';
150
+ if ( function_exists( 'sociallane_build_link_attrs' ) ) {
151
+ $button['attrs'] = sociallane_build_link_attrs( $url_data );
152
+ } else {
153
+ $rel_tokens = [];
154
+ if ( ! empty( $url_data['is_external'] ) ) {
155
+ $attrs[] = 'target="_blank"';
156
+ $rel_tokens[] = 'noopener';
157
+ $rel_tokens[] = 'noreferrer';
158
+ }
159
+ if ( ! empty( $url_data['nofollow'] ) ) {
160
+ $rel_tokens[] = 'nofollow';
161
+ }
162
+ if ( ! empty( $rel_tokens ) ) {
163
+ $attrs[] = 'rel="' . esc_attr( implode( ' ', array_values( array_unique( $rel_tokens ) ) ) ) . '"';
164
+ }
165
+ $button['attrs'] = implode( ' ', $attrs );
153
166
  }
154
- if ( ! empty( $url_data['nofollow'] ) ) {
155
- $attrs[] = 'rel="nofollow"';
156
- }
157
- $button['attrs'] = implode( ' ', $attrs );
158
167
 
159
168
  include __DIR__ . '/templates/render.php';
160
169
  }
@@ -592,7 +592,7 @@ class Widget_Manager {
592
592
  'navigation' => 'navigation',
593
593
  'footers' => 'footer',
594
594
  'logos' => 'logo-grid-centered',
595
- 'dashboards' => 'posts-grid',
595
+ 'dashboards' => 'grid-posts',
596
596
  ];
597
597
  $category = $widget['category'] ?? '';
598
598
  $fallback_slug = $category_fallbacks[ $category ] ?? '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sociallane/elements",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Elementor widgets and elements with Tailwind CSS for SocialLane. WordPress plugin.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -30,6 +30,8 @@
30
30
  "postinstall": "node scripts/postinstall.js",
31
31
  "setup": "bash scripts/setup.sh",
32
32
  "sync-widgets": "node scripts/sync-widgets.js",
33
+ "build:widget-packages": "node scripts/build-widget-packages.js",
34
+ "publish:widgets": "node scripts/build-widget-packages.js && node scripts/publish-widget-packages.js",
33
35
  "reinstall": "rm -rf node_modules && npm install",
34
36
  "format": "prettier --write .",
35
37
  "format:check": "prettier --check ."
@@ -52,6 +52,35 @@ function sociallane_prepare_image_data( array $image_settings, array $options =
52
52
  return $image;
53
53
  }
54
54
 
55
+ /**
56
+ * Build safe target/rel HTML attributes from Elementor URL control data.
57
+ *
58
+ * Ensures rel is emitted once (e.g. rel="noopener noreferrer nofollow")
59
+ * when both external and nofollow are enabled.
60
+ *
61
+ * @param array $url_settings Elementor URL control: url, is_external, nofollow
62
+ * @return string HTML attributes string
63
+ */
64
+ function sociallane_build_link_attrs( array $url_settings ): string {
65
+ $url_settings = is_array( $url_settings ) ? $url_settings : [];
66
+ $attrs = [];
67
+ $rel_tokens = [];
68
+
69
+ if ( ! empty( $url_settings['is_external'] ) ) {
70
+ $attrs[] = 'target="_blank"';
71
+ $rel_tokens[] = 'noopener';
72
+ $rel_tokens[] = 'noreferrer';
73
+ }
74
+ if ( ! empty( $url_settings['nofollow'] ) ) {
75
+ $rel_tokens[] = 'nofollow';
76
+ }
77
+ if ( ! empty( $rel_tokens ) ) {
78
+ $attrs[] = 'rel="' . esc_attr( implode( ' ', array_values( array_unique( $rel_tokens ) ) ) ) . '"';
79
+ }
80
+
81
+ return implode( ' ', $attrs );
82
+ }
83
+
55
84
  /**
56
85
  * Prepare button data for the button element.
57
86
  *
@@ -83,24 +112,14 @@ function sociallane_prepare_button_data( string $text, array $url, array $option
83
112
  $extra_class = $options['class'] ?? '';
84
113
 
85
114
  $href = esc_url( $url['url'] ?? '#' );
86
- $is_external = ! empty( $url['is_external'] );
87
- $nofollow = ! empty( $url['nofollow'] );
88
-
89
- $attrs = [];
90
- if ( $is_external ) {
91
- $attrs[] = 'target="_blank"';
92
- $attrs[] = 'rel="noopener noreferrer"';
93
- }
94
- if ( $nofollow ) {
95
- $attrs[] = 'rel="nofollow"';
96
- }
115
+ $attrs = sociallane_build_link_attrs( $url );
97
116
 
98
117
  $data = [
99
118
  'text' => esc_html( $text ),
100
119
  'url' => $href,
101
120
  'variant' => $variant,
102
121
  'size' => $size,
103
- 'attrs' => implode( ' ', $attrs ),
122
+ 'attrs' => $attrs,
104
123
  ];
105
124
 
106
125
  if ( $color !== 'default' ) {
@@ -269,19 +288,10 @@ function sociallane_stagger_attrs( array $anim ): string {
269
288
  function sociallane_prepare_link_attrs( array $url_settings ): array {
270
289
  $url_settings = is_array( $url_settings ) ? $url_settings : [];
271
290
  $href = esc_url( $url_settings['url'] ?? '#' );
272
- $attrs = [];
273
-
274
- if ( ! empty( $url_settings['is_external'] ) ) {
275
- $attrs[] = 'target="_blank"';
276
- $attrs[] = 'rel="noopener noreferrer"';
277
- }
278
- if ( ! empty( $url_settings['nofollow'] ) ) {
279
- $attrs[] = 'rel="nofollow"';
280
- }
281
291
 
282
292
  return [
283
293
  'url' => $href,
284
- 'attrs' => implode( ' ', $attrs ),
294
+ 'attrs' => sociallane_build_link_attrs( $url_settings ),
285
295
  ];
286
296
  }
287
297
 
@@ -55,24 +55,15 @@ function prepare_footer_brand_view( array $settings, string $widget_id ): array
55
55
  $social_links = [];
56
56
  if ( $show_social ) {
57
57
  foreach ( $settings['social_links'] ?? [] as $item ) {
58
- $url_data = $item['url'] ?? [];
59
- $href = esc_url( $url_data['url'] ?? '#' );
60
- $attrs = [];
61
- if ( ! empty( $url_data['is_external'] ) ) {
62
- $attrs[] = 'target="_blank"';
63
- $attrs[] = 'rel="noopener noreferrer"';
64
- }
65
- if ( ! empty( $url_data['nofollow'] ) ) {
66
- $attrs[] = 'rel="nofollow"';
67
- }
58
+ $link = sociallane_prepare_link_attrs( $item['url'] ?? [] );
68
59
  $aria_label = trim( $item['aria_label'] ?? '' );
69
60
  if ( $aria_label === '' ) {
70
61
  $aria_label = __( 'Link', 'sociallane-elements' );
71
62
  }
72
63
  $social_links[] = [
73
64
  'icon' => $item['icon'] ?? null,
74
- 'href' => $href,
75
- 'attrs' => implode( ' ', $attrs ),
65
+ 'href' => $link['url'],
66
+ 'attrs' => $link['attrs'],
76
67
  'aria_label' => esc_attr( $aria_label ),
77
68
  ];
78
69
  }
@@ -144,7 +144,7 @@ foreach ( $slides as $s ) {
144
144
  </div>
145
145
  <?php if ( ! empty( $view['anchor_button'] ) ) : ?>
146
146
  <a
147
- href="<?php echo $view['anchor_button']['url']; ?>"
147
+ href="<?php echo esc_url( $view['anchor_button']['url'] ); ?>"
148
148
  class="<?php echo esc_attr( $view['classes']['anchor_btn'] ?? '' ); ?>"
149
149
  aria-label="<?php echo esc_attr( $view['anchor_button']['text'] ); ?>"
150
150
  >
@@ -144,7 +144,7 @@ foreach ( $slides as $s ) {
144
144
  </div>
145
145
  <?php if ( ! empty( $view['anchor_button'] ) ) : ?>
146
146
  <a
147
- href="<?php echo $view['anchor_button']['url']; ?>"
147
+ href="<?php echo esc_url( $view['anchor_button']['url'] ); ?>"
148
148
  class="<?php echo esc_attr( $view['classes']['anchor_btn'] ?? '' ); ?>"
149
149
  aria-label="<?php echo esc_attr( $view['anchor_button']['text'] ); ?>"
150
150
  >
@@ -144,7 +144,7 @@ foreach ( $slides as $s ) {
144
144
  </div>
145
145
  <?php if ( ! empty( $view['anchor_button'] ) ) : ?>
146
146
  <a
147
- href="<?php echo $view['anchor_button']['url']; ?>"
147
+ href="<?php echo esc_url( $view['anchor_button']['url'] ); ?>"
148
148
  class="<?php echo esc_attr( $view['classes']['anchor_btn'] ?? '' ); ?>"
149
149
  aria-label="<?php echo esc_attr( $view['anchor_button']['text'] ); ?>"
150
150
  >
@@ -131,13 +131,13 @@ $filter_inactive_classes = ! empty( $classes['filter_inactive'] ) ? explode( ' '
131
131
 
132
132
  if (search) {
133
133
  search.addEventListener('input', function() {
134
- var q = this.value.toLowerCase().trim();
135
- cards.forEach(function(c) {
136
- var show = !q || (c.getAttribute('data-display') || '').toLowerCase().indexOf(q) !== -1 ||
137
- (c.getAttribute('data-name') || '').toLowerCase().indexOf(q) !== -1 ||
138
- (c.getAttribute('data-category') || '').toLowerCase().indexOf(q) !== -1;
139
- c.style.display = show ? '' : 'none';
140
- });
134
+ var q = this.value.toLowerCase().trim();
135
+ cards.forEach(function(c) {
136
+ var show = !q || (c.getAttribute('data-display') || '').toLowerCase().indexOf(q) !== -1 ||
137
+ (c.getAttribute('data-widget-name') || '').toLowerCase().indexOf(q) !== -1 ||
138
+ (c.getAttribute('data-category') || '').toLowerCase().indexOf(q) !== -1;
139
+ c.style.display = show ? '' : 'none';
140
+ });
141
141
  updateCount();
142
142
  });
143
143
  }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build per-widget npm packages under packages/npm-widgets/<slug>.
4
+ * Each package contains: package.json, widget/ (copy of packages/widgets/<slug>), install.js, README.md.
5
+ * Run from plugin root: npm run build:widget-packages
6
+ */
7
+
8
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const pluginRoot = path.resolve(__dirname, '..');
14
+ const widgetsDir = path.join(pluginRoot, 'packages', 'widgets');
15
+ const npmWidgetsDir = path.join(pluginRoot, 'packages', 'npm-widgets');
16
+
17
+ function getWidgetSlugs() {
18
+ if (!existsSync(widgetsDir)) return [];
19
+ const entries = readdirSync(widgetsDir, { withFileTypes: true });
20
+ const slugs = [];
21
+ for (const ent of entries) {
22
+ if (!ent.isDirectory()) continue;
23
+ const slug = ent.name;
24
+ const phpFile = path.join(widgetsDir, slug, `${slug}.php`);
25
+ if (existsSync(phpFile)) slugs.push(slug);
26
+ }
27
+ return slugs.sort((a, b) => a.localeCompare(b));
28
+ }
29
+
30
+ function getRootVersion() {
31
+ const pkgPath = path.join(pluginRoot, 'package.json');
32
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
33
+ return pkg.version || '1.0.0';
34
+ }
35
+
36
+ function buildPackage(slug, version) {
37
+ const pkgDir = path.join(npmWidgetsDir, slug);
38
+ mkdirSync(pkgDir, { recursive: true });
39
+
40
+ const widgetSrc = path.join(widgetsDir, slug);
41
+ const widgetDest = path.join(pkgDir, 'widget');
42
+ cpSync(widgetSrc, widgetDest, { recursive: true, force: true });
43
+
44
+ const packageName = `@sociallane/widget-${slug}`;
45
+ const binName = `widget-${slug}`;
46
+ const packageJson = {
47
+ name: packageName,
48
+ version,
49
+ description: `SocialLane Elements widget: ${slug}. Install into WordPress plugin via npx.`,
50
+ type: 'module',
51
+ private: false,
52
+ bin: { [binName]: 'install.js' },
53
+ keywords: ['wordpress', 'elementor', 'widget', 'sociallane', slug],
54
+ repository: {
55
+ type: 'git',
56
+ url: 'git+https://github.com/Mitch00llK/sociallane-elements.git',
57
+ },
58
+ };
59
+ writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
60
+
61
+ const readme = `# @sociallane/widget-${slug}
62
+
63
+ Elementor widget **${slug}** for SocialLane Elements.
64
+
65
+ ## Install
66
+
67
+ From your WordPress root (or \`wp-content/plugins\`):
68
+
69
+ \`\`\`bash
70
+ npx @sociallane/widget-${slug}
71
+ \`\`\`
72
+
73
+ This installs the SocialLane Elements plugin if needed, then adds this widget and builds.
74
+ `;
75
+ writeFileSync(path.join(pkgDir, 'README.md'), readme);
76
+
77
+ const installerSrc = path.join(pluginRoot, 'scripts', 'widget-installer.js');
78
+ cpSync(installerSrc, path.join(pkgDir, 'install.js'), { force: true });
79
+ }
80
+
81
+ function main() {
82
+ const slugs = getWidgetSlugs();
83
+ const version = getRootVersion();
84
+ if (!existsSync(npmWidgetsDir)) mkdirSync(npmWidgetsDir, { recursive: true });
85
+ for (const slug of slugs) {
86
+ buildPackage(slug, version);
87
+ console.log('Built:', `@sociallane/widget-${slug}`);
88
+ }
89
+ console.log('Done. Widget packages in packages/npm-widgets/');
90
+ }
91
+
92
+ main();
@@ -20,8 +20,25 @@ const widgetsSource = path.join(packageRoot, 'packages', 'widgets');
20
20
 
21
21
  function resolvePluginDir() {
22
22
  const cwd = process.cwd();
23
- const hasPlugin = existsSync(path.join(cwd, 'sociallane-elements.php')) && existsSync(path.join(cwd, 'packages', 'widgets'));
24
- return hasPlugin ? cwd : path.join(cwd, 'wp-content', 'plugins', 'sociallane-elements');
23
+ const normalized = path.normalize(cwd);
24
+ const pluginsSegment = 'wp-content' + path.sep + 'plugins';
25
+ if (existsSync(path.join(cwd, 'sociallane-elements.php')) && existsSync(path.join(cwd, 'packages', 'widgets'))) {
26
+ return cwd;
27
+ }
28
+ if (normalized.endsWith(pluginsSegment) || normalized.endsWith(pluginsSegment + path.sep)) {
29
+ return path.join(cwd, 'sociallane-elements');
30
+ }
31
+ if (normalized.includes(pluginsSegment + path.sep)) {
32
+ const idx = normalized.indexOf(pluginsSegment);
33
+ return path.join(normalized.slice(0, idx + pluginsSegment.length), 'sociallane-elements');
34
+ }
35
+ if (path.basename(cwd) === 'wp-content') {
36
+ return path.join(cwd, 'plugins', 'sociallane-elements');
37
+ }
38
+ if (existsSync(path.join(cwd, 'wp-content', 'plugins'))) {
39
+ return path.join(cwd, 'wp-content', 'plugins', 'sociallane-elements');
40
+ }
41
+ return path.join(cwd, 'wp-content', 'plugins', 'sociallane-elements');
25
42
  }
26
43
 
27
44
  function addWidgets(slugs) {
@@ -109,9 +126,18 @@ function copyPackage(toDir, filter) {
109
126
  function install(targetPath, minimal) {
110
127
  const cwd = process.cwd();
111
128
  const resolveDefaultTarget = () => {
129
+ const normalized = path.normalize(cwd);
130
+ const pluginsSegment = 'wp-content' + path.sep + 'plugins';
112
131
  if (existsSync(path.join(cwd, 'sociallane-elements.php'))) {
113
132
  return cwd;
114
133
  }
134
+ if (normalized.endsWith(pluginsSegment) || normalized.endsWith(pluginsSegment + path.sep)) {
135
+ return path.join(cwd, 'sociallane-elements');
136
+ }
137
+ if (normalized.includes(pluginsSegment + path.sep)) {
138
+ const idx = normalized.indexOf(pluginsSegment);
139
+ return path.join(normalized.slice(0, idx + pluginsSegment.length), 'sociallane-elements');
140
+ }
115
141
  if (path.basename(cwd) === 'plugins' && path.basename(path.dirname(cwd)) === 'wp-content') {
116
142
  return path.join(cwd, 'sociallane-elements');
117
143
  }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Publish all per-widget packages under packages/npm-widgets/.
4
+ * Run from plugin root: npm run publish:widgets
5
+ * (build:widget-packages is run first by the npm script.)
6
+ */
7
+
8
+ import { readdirSync, existsSync } from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { spawnSync } from 'child_process';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const pluginRoot = path.resolve(__dirname, '..');
15
+ const npmWidgetsDir = path.join(pluginRoot, 'packages', 'npm-widgets');
16
+
17
+ if (!existsSync(npmWidgetsDir)) {
18
+ console.error('packages/npm-widgets/ not found. Run npm run build:widget-packages first.');
19
+ process.exit(1);
20
+ }
21
+
22
+ const dirs = readdirSync(npmWidgetsDir, { withFileTypes: true })
23
+ .filter((d) => d.isDirectory())
24
+ .map((d) => d.name)
25
+ .sort((a, b) => a.localeCompare(b));
26
+
27
+ if (dirs.length === 0) {
28
+ console.error('No widget packages in packages/npm-widgets/');
29
+ process.exit(1);
30
+ }
31
+
32
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
33
+ (async () => {
34
+ for (let i = 0; i < dirs.length; i++) {
35
+ const slug = dirs[i];
36
+ const pkgDir = path.join(npmWidgetsDir, slug);
37
+ if (!existsSync(path.join(pkgDir, 'package.json'))) continue;
38
+ console.log('Publishing @sociallane/widget-' + slug + '...');
39
+ const r = spawnSync(npmCmd, ['publish', '--access', 'public'], {
40
+ cwd: pkgDir,
41
+ stdio: 'inherit',
42
+ shell: true,
43
+ });
44
+ if (r.status !== 0) {
45
+ process.exit(r.status ?? 1);
46
+ }
47
+ if (i < dirs.length - 1) {
48
+ await new Promise((r) => setTimeout(r, 2000));
49
+ }
50
+ }
51
+ console.log('Done. Published', dirs.length, 'widget packages.');
52
+ })();
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
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.
6
+ */
7
+
8
+ import { cpSync, existsSync, mkdirSync, readFileSync } from 'fs';
9
+ import { spawnSync } from 'child_process';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const packageRoot = path.resolve(__dirname);
15
+ const widgetDir = path.join(packageRoot, 'widget');
16
+
17
+ function getSlugFromPackage() {
18
+ const pkgPath = path.join(packageRoot, 'package.json');
19
+ if (!existsSync(pkgPath)) {
20
+ console.error('package.json not found in', packageRoot);
21
+ process.exit(1);
22
+ }
23
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
24
+ const name = pkg.name || '';
25
+ const match = name.match(/^@sociallane\/widget-(.+)$/);
26
+ if (!match) {
27
+ console.error('Package name must be @sociallane/widget-<slug>. Got:', name);
28
+ process.exit(1);
29
+ }
30
+ return match[1];
31
+ }
32
+
33
+ function resolvePluginDir() {
34
+ const cwd = process.cwd();
35
+ const normalized = path.normalize(cwd);
36
+ const pluginsSegment = 'wp-content' + path.sep + 'plugins';
37
+ if (existsSync(path.join(cwd, 'sociallane-elements.php')) && existsSync(path.join(cwd, 'packages', 'widgets'))) {
38
+ return cwd;
39
+ }
40
+ if (normalized.endsWith(pluginsSegment) || normalized.endsWith(pluginsSegment + path.sep)) {
41
+ return path.join(cwd, 'sociallane-elements');
42
+ }
43
+ if (normalized.includes(pluginsSegment + path.sep)) {
44
+ const idx = normalized.indexOf(pluginsSegment);
45
+ return path.join(normalized.slice(0, idx + pluginsSegment.length), 'sociallane-elements');
46
+ }
47
+ if (path.basename(cwd) === 'plugins' && path.basename(path.dirname(cwd)) === 'wp-content') {
48
+ return path.join(cwd, 'sociallane-elements');
49
+ }
50
+ if (path.basename(cwd) === 'wp-content') {
51
+ return path.join(cwd, 'plugins', 'sociallane-elements');
52
+ }
53
+ if (existsSync(path.join(cwd, 'wp-content', 'plugins'))) {
54
+ return path.join(cwd, 'wp-content', 'plugins', 'sociallane-elements');
55
+ }
56
+ return path.join(cwd, 'wp-content', 'plugins', 'sociallane-elements');
57
+ }
58
+
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);
68
+ }
69
+
70
+ const pluginDir = resolvePluginDir();
71
+ const widgetsDest = path.join(pluginDir, 'packages', 'widgets');
72
+ const pluginExists = existsSync(path.join(pluginDir, 'sociallane-elements.php'));
73
+
74
+ if (!pluginExists) {
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
+ }
84
+ }
85
+
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
+ }
115
+
116
+ console.log('');
117
+ console.log('Done. Widget', slug, 'installed. Activate the plugin in WordPress → Plugins if needed.');
118
+ }
119
+
120
+ try {
121
+ main();
122
+ } catch (err) {
123
+ console.error('Widget installer failed:', err.message);
124
+ if (process.env.DEBUG) {
125
+ console.error(err.stack);
126
+ }
127
+ process.exit(1);
128
+ }
@@ -47,9 +47,15 @@ function sociallane_elements_admin_notice(): void {
47
47
  * Whether the plugin is in package mode (widgets.json exists).
48
48
  * In package mode, only widget components are available; no admin UI, previews, or management.
49
49
  *
50
+ * To keep widgets.json but use full admin (settings, previews, onboarding), add to wp-config.php:
51
+ * define( 'SOCIALLANE_ELEMENTS_FULL_ADMIN', true );
52
+ *
50
53
  * @return bool
51
54
  */
52
55
  function sociallane_is_package_mode(): bool {
56
+ if ( defined( 'SOCIALLANE_ELEMENTS_FULL_ADMIN' ) && SOCIALLANE_ELEMENTS_FULL_ADMIN ) {
57
+ return false;
58
+ }
53
59
  return file_exists( SOCIALLANE_ELEMENTS_PATH . 'widgets.json' );
54
60
  }
55
61
 
@@ -64,24 +64,15 @@ function prepare_footer_brand_view( array $settings, string $widget_id ): array
64
64
  $social_links = [];
65
65
  if ( $show_social ) {
66
66
  foreach ( $settings['social_links'] ?? [] as $item ) {
67
- $url_data = $item['url'] ?? [];
68
- $href = esc_url( $url_data['url'] ?? '#' );
69
- $attrs = [];
70
- if ( ! empty( $url_data['is_external'] ) ) {
71
- $attrs[] = 'target="_blank"';
72
- $attrs[] = 'rel="noopener noreferrer"';
73
- }
74
- if ( ! empty( $url_data['nofollow'] ) ) {
75
- $attrs[] = 'rel="nofollow"';
76
- }
67
+ $link = sociallane_prepare_link_attrs( $item['url'] ?? [] );
77
68
  $aria_label = trim( $item['aria_label'] ?? '' );
78
69
  if ( $aria_label === '' ) {
79
70
  $aria_label = __( 'Link', 'sociallane-elements' );
80
71
  }
81
72
  $social_links[] = [
82
73
  'icon' => $item['icon'] ?? null,
83
- 'href' => $href,
84
- 'attrs' => implode( ' ', $attrs ),
74
+ 'href' => $link['url'],
75
+ 'attrs' => $link['attrs'],
85
76
  'aria_label' => esc_attr( $aria_label ),
86
77
  ];
87
78
  }
@@ -144,7 +144,7 @@ foreach ( $slides as $s ) {
144
144
  </div>
145
145
  <?php if ( ! empty( $view['anchor_button'] ) ) : ?>
146
146
  <a
147
- href="<?php echo $view['anchor_button']['url']; ?>"
147
+ href="<?php echo esc_url( $view['anchor_button']['url'] ); ?>"
148
148
  class="<?php echo esc_attr( $view['classes']['anchor_btn'] ?? '' ); ?>"
149
149
  aria-label="<?php echo esc_attr( $view['anchor_button']['text'] ); ?>"
150
150
  >